/**************************************************************************** ** ** 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 //////////////////////////////////////////// } }