/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using System.Threading; using JetBrains.UI.Interop; using DWORD = System.UInt32; using UINT = System.UInt32; using HDDEDATA = System.IntPtr; using HCONV = System.IntPtr; using HSZ = System.IntPtr; using BOOL = System.Boolean; using BOOLEAN = System.Byte; using BYTE = System.Byte; namespace JetBrains.Omea.Base { /// /// Implements DDE operations for the Omea application. /// public class Dde : IDisposable { #region Data /// /// Instance cookie of the DDEML instance. /// protected DWORD _instance = 0u; /// /// Here a delegate will be stored that serves as a WINAPI callback. /// This variable keeps a reference to it alive while it's still needed. /// internal PFNCALLBACK _ddecallback = null; /// /// When the DDE subsystem is first accessed, and the first check for an owner thread is performed, /// the calling thread is written in here. All the subsequent calls must occur on the same thread. /// protected static int _threadOwner = 0; #endregion #region Constants /// /// Contains string identifiers for Internet Explorer's service name and topics. /// public class InternetExplorer { /// /// The Microsoft Internet Explorer version 3.0 or higher service identifier (aka application name). /// public static readonly string Service = "iexplore"; /// /// The Microsoft Internet Explorer version below 3.0 service identifier (aka application name). /// public static readonly string ServiceOld = "mosaic"; /// /// Topic for opening a URL. /// public static readonly string TopicOpenUrl = "WWW_OpenURL"; /// /// Topic for opening a URL in a new window. /// public static readonly string TopicOpenUrlNewWindow = "WWW_OpenURLNewWindow"; /// /// A command that should be sent to open a URL in a new window. /// "%1" should be replaced with the actual URI. /// public static readonly string CommandOpenUrlInNewWindow = "\"%1\",,0"; } #endregion #region Construction /// /// Initializes the use of DDE. /// public Dde() { if(_instance != 0) throw new InvalidOperationException("Must be uninitialized."); CheckOwnerThread(); // Initialize as a client _ddecallback = new PFNCALLBACK(DdeCallback); DdeError dwError = DdeInitialize(ref _instance, _ddecallback, (uint)AfCmd.APPCMD_CLIENTONLY, 0u); // TODO: callback collected if(dwError != DdeError.DMLERR_NO_ERROR) throw new DdeException("Failed to initialize DDE.", dwError); } #endregion #region Operations /// /// Initiates a conversation. /// /// Service (or application name) we're connecting to. /// Topic to request the server for. public DdeConversation CreateConversation(string service, string topic) { CheckOwnerThread(); return new DdeConversation(this, service, topic); } #endregion #region Interop Callbacks /// /// The DDE callback function. As we're a client-only DDE app by now, it should not be called at all. At least, for the meaningful purposes. /// protected HDDEDATA DdeCallback(UINT uType, UINT uFmt, HCONV hconv, HDDEDATA hsz1, HDDEDATA hsz2, HDDEDATA hdata, HDDEDATA dwData1, HDDEDATA dwData2) { return IntPtr.Zero; } #endregion #region Interop Accessors [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern DdeError DdeInitialize(ref DWORD pidInst, PFNCALLBACK pfnCallback, DWORD afCmd, DWORD ulRes); [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern BOOL DdeUninitialize(DWORD idInst); internal delegate HDDEDATA PFNCALLBACK(UINT uType, UINT uFmt, HCONV hconv, HDDEDATA hsz1, HDDEDATA hsz2, HDDEDATA hdata, HDDEDATA dwData1, HDDEDATA dwData2); internal enum AfCmd : uint { APPCLASS_MONITOR = 0x00000001u, APPCLASS_STANDARD = 0x00000000u, APPCMD_CLIENTONLY = 0x00000010u, APPCMD_FILTERINITS = 0x00000020u } [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern BOOL DdeKeepStringHandle(DWORD idInst, HSZ hsz); [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern BOOL DdeFreeStringHandle(DWORD idInst, HSZ hsz); [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern HSZ DdeCreateStringHandle(DWORD idInst, byte[] psz, int iCodePage); [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern UINT DdeGetLastError(DWORD idInst); [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern HCONV DdeConnect(DWORD idInst, HSZ hszService, HSZ hszTopic, /*ref CONVCONTEXT pCC*/IntPtr nullpointer); [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern BOOL DdeDisconnect(HCONV hConv); [DllImport("User32.dll", CharSet=CharSet.Ansi)] internal static extern HDDEDATA DdeClientTransaction(byte[] pData, DWORD cbData, HCONV hConv, HSZ hszItem, UINT wFmt, DdeTransaction wType, DWORD dwTimeout, ref DWORD pdwResult); [StructLayout(LayoutKind.Sequential)] internal struct CONVCONTEXT { public UINT cb; public UINT wFlags; public UINT wCountryID; public int iCodePage; public DWORD dwLangID; public DWORD dwSecurity; public SECURITY_QUALITY_OF_SERVICE qos; } /// /// Quality Of Service. /// [StructLayout(LayoutKind.Sequential)] internal struct SECURITY_QUALITY_OF_SERVICE { public DWORD Length; public SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; public SECURITY_CONTEXT_TRACKING_MODE ContextTrackingMode; public BOOLEAN EffectiveOnly; } ; /// /// Impersonation Level /// /// Impersonation level is represented by a pair of bits in Windows. /// If a new impersonation level is added or lowest value is changed from /// 0 to something else, fix the Windows CreateFile call. /// internal enum SECURITY_IMPERSONATION_LEVEL : uint { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } internal enum SECURITY_CONTEXT_TRACKING_MODE : byte { SECURITY_STATIC_TRACKING = 0, SECURITY_DYNAMIC_TRACKING = 1 } internal enum DdeTransaction : uint { XTYPF_NOBLOCK = 0x0002, /* CBR_BLOCK will not work */ XTYPF_NODATA = 0x0004, /* DDE_FDEFERUPD */ XTYPF_ACKREQ = 0x0008, /* DDE_FACKREQ */ XCLASS_MASK = 0xFC00, XCLASS_BOOL = 0x1000, XCLASS_DATA = 0x2000, XCLASS_FLAGS = 0x4000, XCLASS_NOTIFICATION = 0x8000, XTYP_ERROR = (0x0000 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK), XTYP_ADVDATA = (0x0010 | XCLASS_FLAGS), XTYP_ADVREQ = (0x0020 | XCLASS_DATA | XTYPF_NOBLOCK), XTYP_ADVSTART = (0x0030 | XCLASS_BOOL), XTYP_ADVSTOP = (0x0040 | XCLASS_NOTIFICATION), XTYP_EXECUTE = (0x0050 | XCLASS_FLAGS), XTYP_CONNECT = (0x0060 | XCLASS_BOOL | XTYPF_NOBLOCK), XTYP_CONNECT_CONFIRM = (0x0070 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK), XTYP_XACT_COMPLETE = (0x0080 | XCLASS_NOTIFICATION), XTYP_POKE = (0x0090 | XCLASS_FLAGS), XTYP_REGISTER = (0x00A0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK), XTYP_REQUEST = (0x00B0 | XCLASS_DATA), XTYP_DISCONNECT = (0x00C0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK), XTYP_UNREGISTER = (0x00D0 | XCLASS_NOTIFICATION | XTYPF_NOBLOCK), XTYP_WILDCONNECT = (0x00E0 | XCLASS_DATA | XTYPF_NOBLOCK), XTYP_MASK = 0x00F0, XTYP_SHIFT = 4 /* shift to turn XTYP_ into an index */ } public const uint TIMEOUT_ASYNC = 0xFFFFFFFF; #endregion #region Implementation /// /// Locks a DDE string handle by increasing its reference count so that it won't be dropped from the stringtable. /// internal void KeepStringHandle(HSZ handle) { if(!DdeKeepStringHandle(_instance, handle)) throw new DdeException("Cannot lock the string handle.", GetLastError()); } /// /// Unlocks a DDE string handle by decreasing its reference count. /// internal void FreeStringHandle(HSZ handle) { if(!DdeFreeStringHandle(_instance, handle)) throw new DdeException("Cannot release the string handle.", GetLastError()); } internal HSZ CreateStringHandle(string text) { if(text.Length > 255) { Trace.WriteLine(String.Format("A string cannot be wrapped to a DDE string because it's too long for that. It's {0} chars while the limit is 255. The original string is \"{1}\", was truncated down to the required length.", text.Length, text), "[DDE]"); text = text.Substring(0, 255); } byte[] ps = Encoding.ASCII.GetBytes(text); byte[] psz = new byte[ps.Length + 1]; ps.CopyTo(psz, 0); psz[psz.Length - 1] = 0; HSZ handle = DdeCreateStringHandle(_instance, psz, Win32Declarations.CP_WINANSI); // TODO: winneutral // Error checks if(handle == IntPtr.Zero) throw new DdeException("Cannot create a DDE handle of the string.", GetLastError()); DdeError dwErr = GetLastError(); if(dwErr != DdeError.DMLERR_NO_ERROR) throw new DdeException("Cannot create a DDE handle of the string.", dwErr); return handle; } internal DdeError GetLastError() { return (DdeError)DdeGetLastError(_instance); } /// /// Provides access to the session instance handle. /// internal DWORD Instance { get { return _instance; } } /// /// Ensures that the function is being executed on the owning thread (which is the thread on which DDE has been called for the first time). /// internal static void CheckOwnerThread() { if (_threadOwner == 0) // Invoked for the first time, remember the thread { _threadOwner = Win32Declarations.GetCurrentThreadId(); Trace.WriteLine(String.Format("DDE has remembered thread “{0}”#{1} as its owning thread.", Thread.CurrentThread.Name, _threadOwner), "[DDE]"); } else { // There's already an “own thread” recorded, check that it matches the current thread if(Win32Declarations.GetCurrentThreadId() != _threadOwner) throw new InvalidOperationException(String.Format("DDE functions must be accessed from one thread only, the same one on which they were accessed for the first time (#{0} not “{1}”#{2}).", _threadOwner, Thread.CurrentThread.Name, Win32Declarations.GetCurrentThreadId())); } } #endregion #region IDisposable Members public void Dispose() { if(_instance != 0) { DdeUninitialize(_instance); _instance = 0; } GC.SuppressFinalize(this); } ~Dde() { Dispose(); } #endregion } #region DdeException Class — Implements an exception caused by the DDE interop. /// /// Implements an exception caused by the DDE interop. /// public class DdeException : Exception { /// /// The raw DDE Error represented by this exception. /// private readonly DdeError _error; /// /// Maps DDE error codes to the corresponding human-readable error messages. /// private static Hashtable _hashErrorMessages; /// /// Creates an exception that consists of a text message and a raw DDE error code. /// /// Text message. /// Raw error code. public DdeException(string message, DdeError error) : base(message + " " + ErrorToString(error)) { _error = error; } /// /// Creates an exception that consists of a raw DDE error code. /// /// Raw error code. public DdeException(DdeError error) : base(ErrorToString(error)) { _error = error; } static DdeException() { // Initialize the error strings _hashErrorMessages = new Hashtable(); lock(_hashErrorMessages) { _hashErrorMessages[DdeError.DMLERR_NO_ERROR] = "The operation completed successfully."; _hashErrorMessages[DdeError.DMLERR_ADVACKTIMEOUT] = "A request for a synchronous advise transaction has timed out."; _hashErrorMessages[DdeError.DMLERR_BUSY] = "The response to the transaction caused the DDE_FBUSY flag to be set."; _hashErrorMessages[DdeError.DMLERR_DATAACKTIMEOUT] = "A request for a synchronous data transaction has timed out."; _hashErrorMessages[DdeError.DMLERR_DLL_NOT_INITIALIZED] = "A DDEML function was called without first calling the DdeInitialize function, or an invalid instance identifier was passed to a DDEML function."; _hashErrorMessages[DdeError.DMLERR_DLL_USAGE] = "An application initialized as APPCLASS_MONITOR has attempted to perform a Dynamic Data Exchange (DDE) transaction, or an application initialized as APPCMD_CLIENTONLY has attempted to perform server transactions."; _hashErrorMessages[DdeError.DMLERR_EXECACKTIMEOUT] = "A request for a synchronous execute transaction has timed out."; _hashErrorMessages[DdeError.DMLERR_INVALIDPARAMETER] = "A parameter failed to be validated by the DDEML. Some of the possible causes follow: * The application used a data handle initialized with a different item name handle than was required by the transaction. * The application used a data handle that was initialized with a different clipboard data format than was required by the transaction. * The application used a client-side conversation handle with a server-side function or vice versa. * The application used a freed data handle or string handle. * More than one instance of the application used the same object."; _hashErrorMessages[DdeError.DMLERR_LOW_MEMORY] = "A DDEML application has created a prolonged race condition (in which the server application outruns the client), causing large amounts of memory to be consumed."; _hashErrorMessages[DdeError.DMLERR_MEMORY_ERROR] = "A memory allocation has failed."; _hashErrorMessages[DdeError.DMLERR_NO_CONV_ESTABLISHED] = "A client's attempt to establish a conversation has failed."; _hashErrorMessages[DdeError.DMLERR_NOTPROCESSED] = "A transaction has failed."; _hashErrorMessages[DdeError.DMLERR_POKEACKTIMEOUT] = "A request for a synchronous poke transaction has timed out."; _hashErrorMessages[DdeError.DMLERR_POSTMSG_FAILED] = "An internal call to the PostMessage function has failed."; _hashErrorMessages[DdeError.DMLERR_REENTRANCY] = "An application instance with a synchronous transaction already in progress attempted to initiate another synchronous transaction, or the DdeEnableCallback function was called from within a DDEML callback function."; _hashErrorMessages[DdeError.DMLERR_SERVER_DIED] = "A server-side transaction was attempted on a conversation terminated by the client, or the server terminated before completing a transaction."; _hashErrorMessages[DdeError.DMLERR_SYS_ERROR] = "An internal error has occurred in the DDEML."; _hashErrorMessages[DdeError.DMLERR_UNADVACKTIMEOUT] = "A request to end an advise transaction has timed out."; _hashErrorMessages[DdeError.DMLERR_UNFOUND_QUEUE_ID] = "An invalid transaction identifier was passed to a DDEML function. Once the application has returned from an XTYP_XACT_COMPLETE callback, the transaction identifier for that callback function is no longer valid."; } } /// /// The raw DDE Error code. /// public DdeError Error { get { return _error; } } /// /// Provides a human-readable string representation of a DDE raw error code. /// /// DDE error. /// Error string. public static string ErrorToString(DdeError error) { // Return a string message, if available, or a numerical code otherwise object message; lock(_hashErrorMessages) message = _hashErrorMessages[error]; return message != null ? message.ToString() : String.Format("An unknown DDE error {0} has occured.", (uint)error); } /// /// Gets whether the raw DDE error code contained in this exception instance indicates a timeout. /// public bool Timeout { get { return (_error == DdeError.DMLERR_ADVACKTIMEOUT) || (_error == DdeError.DMLERR_DATAACKTIMEOUT) || (_error == DdeError.DMLERR_EXECACKTIMEOUT) || (_error == DdeError.DMLERR_POKEACKTIMEOUT) || (_error == DdeError.DMLERR_UNADVACKTIMEOUT); } } } /// /// DDE error codes. /// public enum DdeError : uint { DMLERR_NO_ERROR = 0, DMLERR_FIRST = 0x4000, DMLERR_ADVACKTIMEOUT = 0x4000, DMLERR_BUSY = 0x4001, DMLERR_DATAACKTIMEOUT = 0x4002, DMLERR_DLL_NOT_INITIALIZED = 0x4003, DMLERR_DLL_USAGE = 0x4004, DMLERR_EXECACKTIMEOUT = 0x4005, DMLERR_INVALIDPARAMETER = 0x4006, DMLERR_LOW_MEMORY = 0x4007, DMLERR_MEMORY_ERROR = 0x4008, DMLERR_NOTPROCESSED = 0x4009, DMLERR_NO_CONV_ESTABLISHED = 0x400a, DMLERR_POKEACKTIMEOUT = 0x400b, DMLERR_POSTMSG_FAILED = 0x400c, DMLERR_REENTRANCY = 0x400d, DMLERR_SERVER_DIED = 0x400e, DMLERR_SYS_ERROR = 0x400f, DMLERR_UNADVACKTIMEOUT = 0x4010, DMLERR_UNFOUND_QUEUE_ID = 0x4011, DMLERR_LAST = 0x4011 } #endregion #region Class DdeConversation /// /// Implements a single conversation between a DDE client and a DDE server. /// public class DdeConversation : IDisposable { #region Data /// /// The DDE manager instance. /// protected Dde _dde; /// /// Handle to the active conversation. /// protected HCONV _handle; #endregion #region Construction /// /// Initiates a conversation. /// /// Service (or application name) we're connecting to. /// Topic to request the server for. internal DdeConversation(Dde dde, string service, string topic) { _dde = dde; /* // Describe the conversation Dde.CONVCONTEXT cc; cc.cb = (uint)Marshal.SizeOf(typeof(Dde.CONVCONTEXT)); cc.wFlags = 0u; cc.wCountryID = 0u; cc.iCodePage = Win32Declarations.CP_WINANSI; cc.dwLangID = 0u; cc.dwSecurity = 0u; cc.qos.Length = (uint)Marshal.SizeOf(typeof(Dde.SECURITY_QUALITY_OF_SERVICE)); cc.qos.ImpersonationLevel = Dde.SECURITY_IMPERSONATION_LEVEL.SecurityAnonymous; cc.qos.ContextTrackingMode = Dde.SECURITY_CONTEXT_TRACKING_MODE.SECURITY_STATIC_TRACKING; cc.qos.EffectiveOnly = 1; */ using(DdeString dsService = new DdeString(_dde, service)) using(DdeString dsTopic = new DdeString(_dde, topic)) _handle = Dde.DdeConnect(_dde.Instance, dsService.Handle, dsTopic.Handle, IntPtr.Zero); if(_handle == IntPtr.Zero) throw new DdeException("Could not start the DDE conversation.", _dde.GetLastError()); } ~DdeConversation() { if(_handle != IntPtr.Zero) Dde.DdeDisconnect(_handle); } #endregion #region Operations /// /// Initiates an asynchronous DDE transaction. /// /// Item name for which the transaction is executed. /// Data to be exchanged during the transaction. /// This function is the same as with timeout set to . public void StartAsyncTransaction(string item, string data) { StartTransaction( item, data, Dde.TIMEOUT_ASYNC ); } /// /// Initiates a synchronous DDE transaction, using the timeout value specified. /// If the timeout expires, an exception is thrown. /// /// Item name for which the transaction is executed. /// Data to be exchanged during the transaction. /// Specifies the maximum length of time, in milliseconds, that the client will wait for a response from the server application in a synchronous transaction. public void StartTransaction(string item, string data, DWORD timeout) { Dde.CheckOwnerThread(); byte[] arDataBuffer = data != null ? Encoding.ASCII.GetBytes(data) : null; DWORD dwResult = 0; using(DdeString dsItem = new DdeString(_dde, item)) { if(Dde.DdeClientTransaction(arDataBuffer, (uint)(arDataBuffer != null ? arDataBuffer.Length : 0), _handle, dsItem.Handle, 0, Dde.DdeTransaction.XTYP_EXECUTE, timeout, ref dwResult) == IntPtr.Zero) throw new DdeException("Could start a DDE transaction.", _dde.GetLastError()); } } #endregion #region IDisposable Members public void Dispose() { if((_handle != IntPtr.Zero) && (_dde != null)) { if(!Dde.DdeDisconnect(_handle)) throw new DdeException("Could not terminate the conversation.", _dde.GetLastError()); _handle = IntPtr.Zero; _dde = null; } GC.SuppressFinalize(this); } #endregion } #endregion #region Class DdeString — Implements a wrapper around the DDE string handle. /// /// Implements a wrapper around the DDE string handle. /// internal class DdeString : IDisposable { /// /// The string value of the DDE string (if assigned or already extracted). /// Null if the object has been created from a handle and the string has not been retrieved yet. /// protected string _string = null; /// /// The DDE object instance. It provides a handle to the DDEML instance. /// private readonly Dde _dde; /// /// DDE handle to the string /// protected HSZ _handle = IntPtr.Zero; /// /// Determines whether the handle should be released (is owned by this object). /// protected bool _release = false; /// /// Initializes the object from a string. The handle will be generated upon a request. /// Null is a valid value for a string, will cause a Null handle to be returned. /// public DdeString(Dde dde, string text) { _string = text; _dde = dde; } /// /// Initializes the object from a handle, takes ownership over the handle, and extracts a string value as it's needed. /// Null is a valid value for the handle, will cause a Null string to be returned. /// public DdeString(Dde dde, HSZ handle) { _dde = dde; _handle = handle; _dde.KeepStringHandle(handle); _release = handle != IntPtr.Zero; } /// /// Gets the string value of the DDE string. /// This value is either assigned by the constructor, or retrieved from the handle. /// public string String { get { if((_string == null) && (_handle == IntPtr.Zero)) return null; // A valid Null value if(_string == null) // A handle is defined, but the string is Null => it has to be retrieved throw new NotImplementedException("Getting a string from the handle is not implemented yet."); // TODO: implement return _string; } } /// /// Gets the handle to the DDE string. This is either a handle passed in to the constructor, or a handle created from a string if a string was passed in. /// public HSZ Handle { get { // Check if handle was not produced from the string yet if(_handle == IntPtr.Zero) { if(_string == null) // It's a valid Null value return IntPtr.Zero; _handle = _dde.CreateStringHandle(_string); _release = true; } return _handle; } } #region IDisposable Members public void Dispose() { _string = null; // Release the DDE handle? if(_release) { if(_handle == IntPtr.Zero) throw new InvalidOperationException("Wanna free the handle, but the handle is null."); _dde.FreeStringHandle(_handle); _release = false; _handle = IntPtr.Zero; } GC.SuppressFinalize(this); } ~DdeString() { if((_release) && (_handle != IntPtr.Zero) && (_dde != null)) _dde.FreeStringHandle(_handle); } #endregion } #endregion }