| /****************************************************************************  | 
| **  | 
| ** Copyright (C) 2018 The Qt Company Ltd.  | 
| ** Contact: https://www.qt.io/licensing/  | 
| **  | 
| ** This file is part of the Qt VS Tools.  | 
| **  | 
| ** $QT_BEGIN_LICENSE:GPL-EXCEPT$  | 
| ** Commercial License Usage  | 
| ** Licensees holding valid commercial Qt licenses may use this file in  | 
| ** accordance with the commercial license agreement provided with the  | 
| ** Software or, alternatively, in accordance with the terms contained in  | 
| ** a written agreement between you and The Qt Company. For licensing terms  | 
| ** and conditions see https://www.qt.io/terms-conditions. For further  | 
| ** information use the contact form at https://www.qt.io/contact-us.  | 
| **  | 
| ** GNU General Public License Usage  | 
| ** Alternatively, this file may be used under the terms of the GNU  | 
| ** General Public License version 3 as published by the Free Software  | 
| ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT  | 
| ** included in the packaging of this file. Please review the following  | 
| ** information to ensure the GNU General Public License requirements will  | 
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html.  | 
| **  | 
| ** $QT_END_LICENSE$  | 
| **  | 
| ****************************************************************************/  | 
|   | 
| using QtVsTools.Options;  | 
| using System;  | 
| using System.Collections.Concurrent;  | 
| using System.IO;  | 
| using System.Net.Sockets;  | 
| using System.Runtime.InteropServices;  | 
| using System.Runtime.Serialization;  | 
| using System.Runtime.Serialization.Json;  | 
| using System.Text;  | 
| using System.Threading;  | 
| using System.Threading.Tasks;  | 
|   | 
| namespace QtVsTools.Qml.Debug.V4  | 
| {  | 
|     enum DebugClientState { Unavailable, Disconnected, Connecting, Connected, Disconnecting }  | 
|   | 
|     interface IConnectionEventSink  | 
|     {  | 
|         bool QueryRuntimeFrozen();  | 
|   | 
|         void NotifyStateTransition(  | 
|             DebugClient client,  | 
|             DebugClientState oldState,  | 
|             DebugClientState newState);  | 
|   | 
|         void NotifyMessageReceived(  | 
|             DebugClient client,  | 
|             string messageType,  | 
|             byte[] messageParams);  | 
|     }  | 
|   | 
|     class DebugClient : Finalizable  | 
|     {  | 
|         IConnectionEventSink sink;  | 
|         IntPtr client;  | 
|         Task clientThread;  | 
|         EventWaitHandle clientCreated = new EventWaitHandle(false, EventResetMode.ManualReset);  | 
|         EventWaitHandle clientConnected;  | 
|   | 
|         public uint? ThreadId { get; private set; }  | 
|   | 
|         DebugClientState state = DebugClientState.Unavailable;  | 
|         public DebugClientState State  | 
|         {  | 
|             get  | 
|             {  | 
|                 if (clientThread == null || clientThread.Status != TaskStatus.Running)  | 
|                     return DebugClientState.Unavailable;  | 
|   | 
|                 return state;  | 
|             }  | 
|             set  | 
|             {  | 
|                 if (state != value) {  | 
|                     var oldState = state;  | 
|                     state = value;  | 
|                     Task.Run(() => sink.NotifyStateTransition(this, oldState, value));  | 
|                 }  | 
|             }  | 
|         }  | 
|   | 
|         public static DebugClient Create(IConnectionEventSink sink)  | 
|         {  | 
|             var _this = new DebugClient();  | 
|             return _this.Initialize(sink) ? _this : null;  | 
|         }  | 
|   | 
|         private DebugClient()  | 
|         { }  | 
|   | 
|         private bool Initialize(IConnectionEventSink sink)  | 
|         {  | 
|             this.sink = sink;  | 
|   | 
|             Task.WaitAny(new[]  | 
|             {  | 
|                 // Try to start client thread  | 
|                 // Unblock if thread was abruptly terminated (e.g. DLL not found)  | 
|                 clientThread = Task.Run(() => ClientThread()),  | 
|   | 
|                 // Unblock if client was created (i.e. client thread is running)  | 
|                 Task.Run(() => clientCreated.WaitOne())  | 
|             });  | 
|   | 
|             if (State == DebugClientState.Unavailable) {  | 
|                 // Client thread did not start  | 
|                 clientCreated.Set();  | 
|                 Dispose();  | 
|                 return false;  | 
|             }  | 
|   | 
|             return true;  | 
|         }  | 
|   | 
|         protected override void DisposeManaged()  | 
|         {  | 
|             clientCreated.Dispose();  | 
|   | 
|             EnterCriticalSection();  | 
|             if (clientConnected != null) {  | 
|                 LeaveCriticalSection();  | 
|                 clientConnected.Dispose();  | 
|             }  | 
|         }  | 
|   | 
|         protected override void DisposeUnmanaged()  | 
|         {  | 
|             if (State != DebugClientState.Unavailable) {  | 
|                 NativeMethods.DebugClientShutdown(client);  | 
|                 clientThread.Wait();  | 
|             }  | 
|         }  | 
|   | 
|         private void ClientThread()  | 
|         {  | 
|             ThreadId = NativeMethods.GetCurrentThreadId();  | 
|   | 
|             var clientCreated =  | 
|                 new NativeMethods.QmlDebugClientCreated(ClientCreated);  | 
|             var clientDestroyed =  | 
|                 new NativeMethods.QmlDebugClientDestroyed(ClientDestroyed);  | 
|             var clientConnected =  | 
|                 new NativeMethods.QmlDebugClientConnected(ClientConnected);  | 
|             var clientDisconnected =  | 
|                 new NativeMethods.QmlDebugClientDisconnected(ClientDisconnected);  | 
|             var clientMessageReceived =  | 
|                 new NativeMethods.QmlDebugClientMessageReceived(ClientMessageReceived);  | 
|             try {  | 
|                 NativeMethods.DebugClientThread(  | 
|                     clientCreated, clientDestroyed,  | 
|                     clientConnected, clientDisconnected,  | 
|                     clientMessageReceived);  | 
|             } finally {  | 
|                 State = DebugClientState.Unavailable;  | 
|                 GC.KeepAlive(clientCreated);  | 
|                 GC.KeepAlive(clientDestroyed);  | 
|                 GC.KeepAlive(clientConnected);  | 
|                 GC.KeepAlive(clientDisconnected);  | 
|                 GC.KeepAlive(clientMessageReceived);  | 
|             }  | 
|         }  | 
|   | 
|         public EventWaitHandle Connect(string hostName, ushort hostPort)  | 
|         {  | 
|             if (State != DebugClientState.Disconnected)  | 
|                 return null;  | 
|   | 
|             clientConnected = new EventWaitHandle(false, EventResetMode.ManualReset);  | 
|             State = DebugClientState.Connecting;  | 
|             if (string.IsNullOrEmpty(hostName))  | 
|                 hostName = "localhost";  | 
|             var hostNameData = Encoding.UTF8.GetBytes(hostName);  | 
|   | 
|             uint timeout = (uint)QtVsToolsPackage.Instance.Options.QmlDebuggerTimeout;  | 
|             Task.Run(() =>  | 
|             {  | 
|                 var connectTimer = new System.Diagnostics.Stopwatch();  | 
|                 connectTimer.Start();  | 
|   | 
|                 var probe = new Socket(  | 
|                     AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  | 
|                 while (!probe.Connected  | 
|                     && (timeout == 0 || connectTimer.ElapsedMilliseconds < timeout)) {  | 
|                     try {  | 
|                         probe.Connect(hostName, hostPort);  | 
|                     } catch {  | 
|                         Thread.Sleep(3000);  | 
|                     }  | 
|                 }  | 
|   | 
|                 if (probe.Connected) {  | 
|                     probe.Disconnect(false);  | 
|   | 
|                     NativeMethods.DebugClientConnect(client,  | 
|                         hostNameData, hostNameData.Length, hostPort);  | 
|                     connectTimer.Restart();  | 
|   | 
|                     uint connectionTimeout = Math.Max(3000, timeout / 20);  | 
|                     while (!clientConnected.WaitOne(1000)) {  | 
|   | 
|                         if (sink.QueryRuntimeFrozen()) {  | 
|                             connectTimer.Restart();  | 
|   | 
|                         } else {  | 
|                             if (connectTimer.ElapsedMilliseconds > connectionTimeout) {  | 
|                                 if (!Disposing)  | 
|                                     clientConnected.Set();  | 
|   | 
|                                 if (Atomic(() => State == DebugClientState.Connected,  | 
|                                            () => State = DebugClientState.Disconnecting)) {  | 
|                                     NativeMethods.DebugClientDisconnect(client);  | 
|                                 }  | 
|   | 
|                             } else {  | 
|                                 NativeMethods.DebugClientConnect(client,  | 
|                                     hostNameData, hostNameData.Length, hostPort);  | 
|                             }  | 
|                         }  | 
|                     }  | 
|                 }  | 
|             });  | 
|   | 
|             return clientConnected;  | 
|         }  | 
|   | 
|         public EventWaitHandle StartLocalServer(string fileName)  | 
|         {  | 
|             if (State != DebugClientState.Disconnected)  | 
|                 return null;  | 
|   | 
|             clientConnected = new EventWaitHandle(false, EventResetMode.ManualReset);  | 
|             State = DebugClientState.Connecting;  | 
|             var fileNameData = Encoding.UTF8.GetBytes(fileName);  | 
|             if (!NativeMethods.DebugClientStartLocalServer(client,  | 
|                 fileNameData, fileNameData.Length)) {  | 
|                 return null;  | 
|             }  | 
|   | 
|             uint timeout = (uint)QtVsToolsPackage.Instance.Options.QmlDebuggerTimeout;  | 
|             if (timeout != 0) {  | 
|                 Task.Run(() =>  | 
|                 {  | 
|                     var connectTimer = new System.Diagnostics.Stopwatch();  | 
|                     connectTimer.Start();  | 
|   | 
|                     while (!clientConnected.WaitOne(100)) {  | 
|   | 
|                         if (sink.QueryRuntimeFrozen()) {  | 
|                             connectTimer.Restart();  | 
|   | 
|                         } else {  | 
|                             if (connectTimer.ElapsedMilliseconds > timeout) {  | 
|                                 if (!Disposing)  | 
|                                     clientConnected.Set();  | 
|   | 
|                                 if (Atomic(() => State == DebugClientState.Connected,  | 
|                                            () => State = DebugClientState.Disconnecting)) {  | 
|                                     NativeMethods.DebugClientDisconnect(client);  | 
|                                 }  | 
|                             }  | 
|                         }  | 
|                     }  | 
|                 });  | 
|             }  | 
|   | 
|             return clientConnected;  | 
|         }  | 
|   | 
|         public bool Disconnect()  | 
|         {  | 
|             if (State != DebugClientState.Connected)  | 
|                 return false;  | 
|             State = DebugClientState.Disconnecting;  | 
|             return NativeMethods.DebugClientDisconnect(client);  | 
|         }  | 
|   | 
|         public bool SendMessage(string messageType, byte[] messageParams)  | 
|         {  | 
|             if (State != DebugClientState.Connected)  | 
|                 return false;  | 
|             var messageTypeData = Encoding.UTF8.GetBytes(messageType);  | 
|             if (messageParams == null)  | 
|                 messageParams = new byte[0];  | 
|   | 
|             System.Diagnostics.Debug.WriteLine(string.Format(">> {0} {1}",  | 
|                 messageType, Encoding.UTF8.GetString(messageParams)));  | 
|   | 
|             return NativeMethods.DebugClientSendMessage(client,  | 
|                 messageTypeData, messageTypeData.Length,  | 
|                 messageParams, messageParams.Length);  | 
|         }  | 
|   | 
|         void ClientCreated(IntPtr qmlDebugClient)  | 
|         {  | 
|             if (client != IntPtr.Zero || Disposing)  | 
|                 return;  | 
|   | 
|             client = qmlDebugClient;  | 
|             State = DebugClientState.Disconnected;  | 
|             clientCreated.Set();  | 
|         }  | 
|   | 
|         void ClientDestroyed(IntPtr qmlDebugClient)  | 
|         {  | 
|             if (qmlDebugClient != client)  | 
|                 return;  | 
|             State = DebugClientState.Unavailable;  | 
|         }  | 
|   | 
|         void ClientConnected(IntPtr qmlDebugClient)  | 
|         {  | 
|             if (qmlDebugClient != client || Disposing)  | 
|                 return;  | 
|             State = DebugClientState.Connected;  | 
|             clientConnected.Set();  | 
|         }  | 
|   | 
|         void ClientDisconnected(IntPtr qmlDebugClient)  | 
|         {  | 
|             if (qmlDebugClient != client)  | 
|                 return;  | 
|             State = DebugClientState.Disconnected;  | 
|         }  | 
|   | 
|         void ClientMessageReceived(  | 
|             IntPtr qmlDebugClient,  | 
|             byte[] messageTypeData,  | 
|             int messageTypeLength,  | 
|             byte[] messageParamsData,  | 
|             int messageParamsLength)  | 
|         {  | 
|             if (Disposed)  | 
|                 return;  | 
|             if (qmlDebugClient != client)  | 
|                 return;  | 
|             var messageType = Encoding.UTF8.GetString(messageTypeData);  | 
|   | 
|             System.Diagnostics.Debug.WriteLine(string.Format("<< {0} {1}",  | 
|                 messageType, Encoding.UTF8.GetString(messageParamsData)));  | 
|   | 
|             sink.NotifyMessageReceived(this, messageType, messageParamsData);  | 
|         }  | 
|   | 
|         #region //////////////////// Native Methods ///////////////////////////////////////////////  | 
|   | 
|         internal static class NativeMethods  | 
|         {  | 
|             public delegate void QmlDebugClientCreated(IntPtr qmlDebugClient);  | 
|             public delegate void QmlDebugClientDestroyed(IntPtr qmlDebugClient);  | 
|             public delegate void QmlDebugClientConnected(IntPtr qmlDebugClient);  | 
|             public delegate void QmlDebugClientDisconnected(IntPtr qmlDebugClient);  | 
|             public delegate void QmlDebugClientMessageReceived(  | 
|                 IntPtr qmlDebugClient,  | 
|                 [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] messageTypeData,  | 
|                 int messageTypeLength,  | 
|                 [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 4)] byte[] messageParamsData,  | 
|                 int messageParamsLength);  | 
|   | 
|             [DllImport("vsqml",  | 
|                 CallingConvention = CallingConvention.Cdecl,  | 
|                 EntryPoint = "qmlDebugClientThread")]  | 
|             public static extern bool DebugClientThread(  | 
|                 QmlDebugClientCreated clientCreated,  | 
|                 QmlDebugClientDestroyed clientDestroyed,  | 
|                 QmlDebugClientConnected clientConnected,  | 
|                 QmlDebugClientDisconnected clientDisconnected,  | 
|                 QmlDebugClientMessageReceived clientMessageReceived);  | 
|   | 
|             [DllImport("vsqml",  | 
|                 CallingConvention = CallingConvention.Cdecl,  | 
|                 EntryPoint = "qmlDebugClientDisconnect")]  | 
|             public static extern bool DebugClientDisconnect(IntPtr qmlDebugClient);  | 
|   | 
|             [DllImport("vsqml",  | 
|                 CallingConvention = CallingConvention.Cdecl,  | 
|                 EntryPoint = "qmlDebugClientConnect")]  | 
|             public static extern bool DebugClientConnect(  | 
|                 IntPtr qmlDebugClient,  | 
|                 [MarshalAs(UnmanagedType.LPArray)] byte[] hostNameData,  | 
|                 int hostNameLength,  | 
|                 ushort hostPort);  | 
|   | 
|             [DllImport("vsqml",  | 
|                 CallingConvention = CallingConvention.Cdecl,  | 
|                 EntryPoint = "qmlDebugClientStartLocalServer")]  | 
|             public static extern bool DebugClientStartLocalServer(  | 
|                 IntPtr qmlDebugClient,  | 
|                 [MarshalAs(UnmanagedType.LPArray)] byte[] fileNameData,  | 
|                 int fileNameLength);  | 
|   | 
|             [DllImport("vsqml",  | 
|                 CallingConvention = CallingConvention.Cdecl,  | 
|                 EntryPoint = "qmlDebugClientSendMessage")]  | 
|             public static extern bool DebugClientSendMessage(  | 
|                 IntPtr qmlDebugClient,  | 
|                 [MarshalAs(UnmanagedType.LPArray)] byte[] messageTypeData,  | 
|                 int messageTypeLength,  | 
|                 [MarshalAs(UnmanagedType.LPArray)] byte[] messageParamsData,  | 
|                 int messageParamsLength);  | 
|   | 
|             [DllImport("vsqml",  | 
|                 CallingConvention = CallingConvention.Cdecl,  | 
|                 EntryPoint = "qmlDebugClientShutdown")]  | 
|             public static extern bool DebugClientShutdown(IntPtr qmlDebugClient);  | 
|   | 
|             [DllImport("kernel32.dll")]  | 
|             public static extern uint GetCurrentThreadId();  | 
|         }  | 
|   | 
|         #endregion //////////////////// Native Methods ////////////////////////////////////////////  | 
|   | 
|     }  | 
| }  |