| /****************************************************************************  | 
| **  | 
| ** 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 System;  | 
| using System.Collections.Generic;  | 
| using System.IO;  | 
| using System.Linq;  | 
| using System.Threading.Tasks;  | 
| using QtVsTools.SyntaxAnalysis;  | 
| using static QtVsTools.SyntaxAnalysis.RegExpr;  | 
| using RegExprParser = QtVsTools.SyntaxAnalysis.RegExpr.Parser;  | 
|   | 
| namespace QtVsTools.Qml.Debug  | 
| {  | 
|     using V4;  | 
|   | 
|     struct FrameInfo  | 
|     {  | 
|         public int Number;  | 
|         public string QrcPath;  | 
|         public int Line;  | 
|         public string Name;  | 
|         public List<int> Scopes;  | 
|     }  | 
|   | 
|     interface IDebuggerEventSink  | 
|     {  | 
|         bool QueryRuntimeFrozen();  | 
|         void NotifyClientDisconnected();  | 
|         void NotifyStackContext(IList<FrameInfo> frames);  | 
|         void NotifyBreak();  | 
|         void NotifyError(string errorMessage);  | 
|     }  | 
|   | 
|     interface IBreakpoint  | 
|     {  | 
|         string QrcPath { get; }  | 
|         uint Line { get; }  | 
|         void NotifySet();  | 
|         void NotifyClear();  | 
|         void NotifyBreak();  | 
|         void NotifyError(string errorMessage);  | 
|     }  | 
|   | 
|     class QmlDebugger : Disposable, IMessageEventSink  | 
|     {  | 
|         IDebuggerEventSink sink;  | 
|         ProtocolDriver driver;  | 
|         string connectionHostName;  | 
|         ushort connectionHostPortFrom;  | 
|         ushort connectionHostPortTo;  | 
|         string connectionFileName;  | 
|         bool connectionBlock;  | 
|   | 
|         List<Request> outbox;  | 
|         Dictionary<int, IBreakpoint> breakpoints;  | 
|   | 
|         public bool Started { get; private set; }  | 
|   | 
|         public bool Running { get; private set; }  | 
|   | 
|         public string Version { get; private set; }  | 
|   | 
|         public uint? ThreadId { get { return driver.ThreadId; } }  | 
|   | 
|         public static QmlDebugger Create(IDebuggerEventSink sink, string execPath, string args)  | 
|         {  | 
|             var _this = new QmlDebugger();  | 
|             return _this.Initialize(sink, execPath, args) ? _this : null;  | 
|         }  | 
|   | 
|         private QmlDebugger()  | 
|         { }  | 
|   | 
|         private bool Initialize(IDebuggerEventSink sink, string execPath, string args)  | 
|         {  | 
|             this.sink = sink;  | 
|             if (sink == null)  | 
|                 return false;  | 
|   | 
|             if (!ParseCommandLine(execPath, args,  | 
|                 out connectionHostPortFrom, out connectionHostPortTo,  | 
|                 out connectionHostName, out connectionFileName, out connectionBlock)) {  | 
|                 return false;  | 
|             }  | 
|   | 
|             driver = ProtocolDriver.Create(this);  | 
|             if (driver == null)  | 
|                 return false;  | 
|   | 
|             outbox = new List<Request>();  | 
|             breakpoints = new Dictionary<int, IBreakpoint>();  | 
|             return true;  | 
|         }  | 
|   | 
|         protected override void DisposeManaged()  | 
|         {  | 
|             driver.Dispose();  | 
|         }  | 
|   | 
|         void ConnectToDebugger()  | 
|         {  | 
|             if (!string.IsNullOrEmpty(connectionFileName))  | 
|                 driver.StartLocalServer(connectionFileName).WaitOne();  | 
|             else  | 
|                 driver.Connect(connectionHostName, connectionHostPortFrom).WaitOne();  | 
|   | 
|             if (driver.ConnectionState != DebugClientState.Connected) {  | 
|                 sink.NotifyClientDisconnected();  | 
|                 return;  | 
|             }  | 
|   | 
|             var reqVersion = Message.Create<VersionRequest>(driver);  | 
|             var resVersion = reqVersion.Send();  | 
|             if (resVersion != null)  | 
|                 ThreadSafe(() => Version = resVersion.Body.Version);  | 
|   | 
|             foreach (var request in ThreadSafe(() => outbox.ToList()))  | 
|                 request.Send();  | 
|   | 
|             ThreadSafe(() => outbox.Clear());  | 
|   | 
|             Message.Send<ConnectMessage>(driver);  | 
|         }  | 
|   | 
|         bool IMessageEventSink.QueryRuntimeFrozen()  | 
|         {  | 
|             return sink.QueryRuntimeFrozen();  | 
|         }  | 
|   | 
|         public void Run()  | 
|         {  | 
|             EnterCriticalSection();  | 
|   | 
|             if (!Started) {  | 
|                 Running = Started = true;  | 
|                 LeaveCriticalSection();  | 
|                 Task.Run(() => ConnectToDebugger());  | 
|   | 
|             } else if (!Running) {  | 
|                 Running = true;  | 
|                 LeaveCriticalSection();  | 
|                 Request.Send<ContinueRequest>(driver);  | 
|   | 
|             } else {  | 
|                 LeaveCriticalSection();  | 
|             }  | 
|         }  | 
|   | 
|         public void StepOver()  | 
|         {  | 
|             var reqContinue = Message.Create<ContinueRequest>(driver);  | 
|             reqContinue.Arguments.StepAction = ContinueRequest.StepAction.Next;  | 
|             reqContinue.Send();  | 
|         }  | 
|   | 
|         public void StepInto()  | 
|         {  | 
|             var reqContinue = Message.Create<ContinueRequest>(driver);  | 
|             reqContinue.Arguments.StepAction = ContinueRequest.StepAction.StepIn;  | 
|             reqContinue.Send();  | 
|         }  | 
|   | 
|         public void StepOut()  | 
|         {  | 
|             var reqContinue = Message.Create<ContinueRequest>(driver);  | 
|             reqContinue.Arguments.StepAction = ContinueRequest.StepAction.StepOut;  | 
|             reqContinue.Send();  | 
|         }  | 
|   | 
|         public void SetBreakpoint(IBreakpoint breakpoint)  | 
|         {  | 
|             var setBreakpoint = Message.Create<SetBreakpointRequest>(driver);  | 
|             setBreakpoint.Arguments.TargetType = SetBreakpointRequest.TargetType.ScriptRegExp;  | 
|             setBreakpoint.Arguments.Target = breakpoint.QrcPath;  | 
|             setBreakpoint.Arguments.Line = (int)breakpoint.Line;  | 
|             setBreakpoint.Tag = breakpoint;  | 
|             if (driver.ConnectionState == DebugClientState.Connected)  | 
|                 setBreakpoint.SendAsync();  | 
|             else  | 
|                 ThreadSafe(() => outbox.Add(setBreakpoint));  | 
|         }  | 
|   | 
|         void SetBreakpointResponded(SetBreakpointRequest reqSetBreak)  | 
|         {  | 
|             System.Diagnostics.Debug.Assert(reqSetBreak.Response != null);  | 
|   | 
|             var breakpoint = reqSetBreak.Tag as IBreakpoint;  | 
|             System.Diagnostics.Debug.Assert(breakpoint != null);  | 
|   | 
|             if (reqSetBreak.Response.Success) {  | 
|                 ThreadSafe(() => breakpoints[reqSetBreak.Response.Body.Breakpoint] = breakpoint);  | 
|                 breakpoint.NotifySet();  | 
|             } else {  | 
|                 breakpoint.NotifyError(reqSetBreak.Response.Message);  | 
|             }  | 
|         }  | 
|   | 
|         public void ClearBreakpoint(IBreakpoint breakpoint)  | 
|         {  | 
|             var breakpointNum = ThreadSafe(() => breakpoints  | 
|                 .ToDictionary(x => x.Value, x => x.Key));  | 
|   | 
|             if (!breakpointNum.ContainsKey(breakpoint))  | 
|                 return;  | 
|   | 
|             var reqClearBreak = Message.Create<ClearBreakpointRequest>(driver);  | 
|             reqClearBreak.Arguments.Breakpoint = breakpointNum[breakpoint];  | 
|             reqClearBreak.SendAsync();  | 
|         }  | 
|   | 
|         void RefreshFrames()  | 
|         {  | 
|             var frames = new List<FrameInfo>();  | 
|             currentScope = null;  | 
|   | 
|             var reqBacktrace = Message.Create<BacktraceRequest>(driver);  | 
|             var resBacktrace = reqBacktrace.Send();  | 
|             if (resBacktrace != null && resBacktrace.Success) {  | 
|   | 
|                 foreach (var frameRef in resBacktrace.Body.Frames) {  | 
|                     var reqFrame = Message.Create<FrameRequest>(driver);  | 
|                     reqFrame.Arguments.FrameNumber = frameRef.Index;  | 
|   | 
|                     var resFrame = reqFrame.Send();  | 
|                     if (resFrame == null)  | 
|                         continue;  | 
|   | 
|                     var frame = new FrameInfo  | 
|                     {  | 
|                         Number = resFrame.Frame.Index,  | 
|                         Name = resFrame.Frame.Function,  | 
|                         QrcPath = resFrame.Frame.Script,  | 
|                         Line = resFrame.Frame.Line,  | 
|                         Scopes = new List<int>()  | 
|                     };  | 
|   | 
|                     foreach (var scope in resFrame.Frame.Scopes  | 
|                         .Where(x => x.Type != Scope.ScopeType.Global)) {  | 
|                         frame.Scopes.Add(scope.Index);  | 
|                     }  | 
|   | 
|                     frames.Add(frame);  | 
|                 }  | 
|             } else if (resBacktrace != null) {  | 
|                 sink.NotifyError(resBacktrace.Message);  | 
|             } else {  | 
|                 sink.NotifyError("Error sending 'backtrace' message to QML runtime.");  | 
|             }  | 
|             sink.NotifyStackContext(frames);  | 
|         }  | 
|   | 
|         void BreakNotified(BreakEvent evtBreak)  | 
|         {  | 
|             Running = false;  | 
|   | 
|             RefreshFrames();  | 
|   | 
|             if (evtBreak.Body.Breakpoints == null || evtBreak.Body.Breakpoints.Count == 0) {  | 
|                 sink.NotifyBreak();  | 
|   | 
|             } else {  | 
|                 foreach (int breakpointId in evtBreak.Body.Breakpoints) {  | 
|                     IBreakpoint breakpoint;  | 
|                     if (!breakpoints.TryGetValue(breakpointId, out breakpoint))  | 
|                         continue;  | 
|                     breakpoint.NotifyBreak();  | 
|                 }  | 
|             }  | 
|         }  | 
|   | 
|         Scope currentScope = null;  | 
|   | 
|         Scope MoveToScope(int frameNumber, int scopeNumber)  | 
|         {  | 
|             lock (CriticalSection) {  | 
|                 if (currentScope != null  | 
|                     && currentScope.FrameIndex == frameNumber  | 
|                     && currentScope.Index == scopeNumber) {  | 
|                     return currentScope;  | 
|                 }  | 
|   | 
|                 var reqScope = Message.Create<ScopeRequest>(driver);  | 
|                 reqScope.Arguments.FrameNumber = frameNumber;  | 
|                 reqScope.Arguments.ScopeNumber = scopeNumber;  | 
|   | 
|                 var resScope = reqScope.Send();  | 
|                 if (resScope == null)  | 
|                     return null;  | 
|   | 
|                 return currentScope = resScope.Scope;  | 
|             }  | 
|         }  | 
|   | 
|         public IEnumerable<JsValue> RefreshScope(  | 
|             int frameNumber,  | 
|             int scopeNumber,  | 
|             bool forceScope = false)  | 
|         {  | 
|             if (forceScope)  | 
|                 currentScope = null;  | 
|   | 
|             var vars = new SortedList<string, JsValue>();  | 
|             lock (CriticalSection) {  | 
|   | 
|                 var scope = MoveToScope(frameNumber, scopeNumber);  | 
|                 if (scope == null)  | 
|                     return null;  | 
|   | 
|                 var scopeObj = ((JsValue)scope.Object) as JsObject;  | 
|                 if (scopeObj == null)  | 
|                     return null;  | 
|   | 
|                 scopeObj.Properties  | 
|                     .Where(x => x.HasData && !string.IsNullOrEmpty(((JsValue)x).Name))  | 
|                     .Select(x => new { name = ((JsValue)x).Name, value = (JsValue)x })  | 
|                     .ToList().ForEach(x => vars.Add(x.name, x.value));  | 
|   | 
|                 if (scope.Type == Scope.ScopeType.Local) {  | 
|                     var reqEval = Message.Create<EvaluateRequest>(driver);  | 
|                     reqEval.Arguments.Expression = "this";  | 
|                     reqEval.Arguments.Frame = frameNumber;  | 
|   | 
|                     var resEval = reqEval.Send();  | 
|                     if (resEval != null && resEval.Result.HasData) {  | 
|                         JsValue resValue = resEval.Result;  | 
|                         resValue.Name = "this";  | 
|                         vars.Add(resValue.Name, resValue);  | 
|                     }  | 
|                 }  | 
|             }  | 
|             return vars.Values;  | 
|         }  | 
|   | 
|         public JsObject Lookup(int frameNumber, int scopeNumber, JsObjectRef objRef)  | 
|         {  | 
|             if (MoveToScope(frameNumber, scopeNumber) == null)  | 
|                 return null;  | 
|   | 
|             var reqLookup = Message.Create<LookupRequest>(driver);  | 
|             reqLookup.Arguments.Handles = new List<int> { objRef.Ref };  | 
|   | 
|             var resLookup = reqLookup.Send();  | 
|             if (resLookup == null)  | 
|                 return null;  | 
|   | 
|             var defObj = resLookup.Objects.Values.FirstOrDefault();  | 
|             if (!defObj.HasData)  | 
|                 return null;  | 
|   | 
|             JsValue obj = defObj;  | 
|             if (!(obj is JsObject))  | 
|                 return null;  | 
|   | 
|             obj.Name = objRef.Name;  | 
|             return obj as JsObject;  | 
|         }  | 
|   | 
|         public JsValue Evaluate(int frameNumber, string expression)  | 
|         {  | 
|             var reqEval = Message.Create<EvaluateRequest>(driver);  | 
|             reqEval.Arguments.Expression = expression;  | 
|             reqEval.Arguments.Frame = frameNumber;  | 
|   | 
|             var resEval = reqEval.Send();  | 
|             if (resEval == null)  | 
|                 return new JsError { Message = "ERROR: Expression evaluation failed" };  | 
|             if (!resEval.Success)  | 
|                 return new JsError { Message = resEval.Message };  | 
|   | 
|             if (!resEval.Result.HasData)  | 
|                 return new JsError { Message = "ERROR: Cannot read data" };  | 
|   | 
|             return resEval.Result;  | 
|         }  | 
|   | 
|         void IMessageEventSink.NotifyStateTransition(  | 
|             DebugClient client,  | 
|             DebugClientState oldState,  | 
|             DebugClientState newState)  | 
|         {  | 
|             if (oldState != DebugClientState.Unavailable  | 
|                 && newState == DebugClientState.Disconnected) {  | 
|                 Task.Run(() => sink.NotifyClientDisconnected());  | 
|             }  | 
|         }  | 
|   | 
|         void IMessageEventSink.NotifyRequestResponded(Request msgRequest)  | 
|         {  | 
|             if (msgRequest is SetBreakpointRequest)  | 
|                 Task.Run(() => SetBreakpointResponded(msgRequest as SetBreakpointRequest));  | 
|         }  | 
|   | 
|         void IMessageEventSink.NotifyEvent(Event msgEvent)  | 
|         {  | 
|             if (msgEvent is BreakEvent)  | 
|                 Task.Run(() => BreakNotified(msgEvent as BreakEvent));  | 
|         }  | 
|   | 
|         void IMessageEventSink.NotifyMessage(Message msg)  | 
|         {  | 
|             System.Diagnostics.Debug  | 
|                 .Assert(msg is ConnectMessage, "Unexpected message");  | 
|         }  | 
|   | 
|         public static bool CheckCommandLine(string execPath, string args)  | 
|         {  | 
|             ushort portFrom;  | 
|             ushort portTo;  | 
|             string hostName;  | 
|             string fileName;  | 
|             bool block;  | 
|             return ParseCommandLine(  | 
|                 execPath, args, out portFrom, out portTo, out hostName, out fileName, out block);  | 
|         }  | 
|   | 
|         /// <summary>  | 
|         /// Connection parameters for QML debug session  | 
|         /// </summary>  | 
|         class ConnectParams  | 
|         {  | 
|             public ushort Port { get; set; }  | 
|             public ushort? MaxPort { get; set; }  | 
|             public string Host { get; set; }  | 
|             public string File { get; set; }  | 
|             public bool Block { get; set; }  | 
|         }  | 
|   | 
|         enum TokenId { ConnectParams, Port, MaxPort, Host, File, Block }  | 
|   | 
|         /// <summary>  | 
|         /// Regex-based parser for QML debug connection parameters  | 
|         /// </summary>  | 
|         static RegExprParser ConnectParamsParser => _ConnectParamsParser ?? (  | 
|             _ConnectParamsParser = new Token(TokenId.ConnectParams, RxConnectParams)  | 
|             {  | 
|                 new Rule<ConnectParams>  | 
|                 {  | 
|                     Update(TokenId.Port, (ConnectParams conn, ushort n) => conn.Port = n),  | 
|                     Update(TokenId.MaxPort, (ConnectParams conn, ushort n) => conn.MaxPort = n),  | 
|                     Update(TokenId.Host, (ConnectParams conn, string s) => conn.Host = s),  | 
|                     Update(TokenId.File, (ConnectParams conn, string s) => conn.File = s),  | 
|                     Update(TokenId.Block, (ConnectParams conn, bool b) => conn.Block = b)  | 
|                 }  | 
|             }  | 
|             .Render());  | 
|         static RegExprParser _ConnectParamsParser;  | 
|   | 
|         /// <summary>  | 
|         /// Regular expression for parsing connection parameters string in the form:  | 
|         ///  | 
|         ///   -qmljsdebugger=port:<port_num>[,port_max][,host:<address>][,file:<name>][,block]  | 
|         ///  | 
|         /// </summary>  | 
|         static RegExpr RxConnectParams =>  | 
|             "-qmljsdebugger="  | 
|             & ((RxPort | RxHost | RxFile) & RxDelim).Repeat(atLeast: 1) & RxBlock.Optional();  | 
|   | 
|         static RegExpr RxPort =>  | 
|             "port:" & new Token(TokenId.Port, CharDigit.Repeat(atLeast: 1))  | 
|             {  | 
|                 new Rule<ushort> { Capture(token => ushort.Parse(token)) }  | 
|             }  | 
|             & (  | 
|                 "," & new Token(TokenId.MaxPort, CharDigit.Repeat(atLeast: 1))  | 
|                 {  | 
|                     new Rule<ushort> { Capture(token => ushort.Parse(token)) }  | 
|                 }  | 
|             ).Optional();  | 
|   | 
|         static RegExpr RxHost =>  | 
|             "host:" & new Token(TokenId.Host, (~CharSet[CharSpace, Chars[","]]).Repeat(atLeast: 1));  | 
|   | 
|         static RegExpr RxFile =>  | 
|             "file:" & new Token(TokenId.File, (~CharSet[CharSpace, Chars[","]]).Repeat(atLeast: 1));  | 
|   | 
|         static RegExpr RxBlock =>  | 
|             new Token(TokenId.Block, "block")  | 
|             {  | 
|                 new Rule<bool> { Capture(token => true) }  | 
|             };  | 
|   | 
|         static RegExpr RxDelim =>  | 
|             ("," & !LookAhead[CharSpace | EndOfLine]) | LookAhead[CharSpace | EndOfLine];  | 
|   | 
|         /// <summary>  | 
|         /// Extract QML debug connection parameters from command line args  | 
|         /// </summary>  | 
|         public static bool ParseCommandLine(  | 
|             string execPath,  | 
|             string args,  | 
|             out ushort portFrom,  | 
|             out ushort portTo,  | 
|             out string hostName,  | 
|             out string fileName,  | 
|             out bool block)  | 
|         {  | 
|             portFrom = portTo = 0;  | 
|             hostName = fileName = "";  | 
|             block = false;  | 
|   | 
|             ConnectParams connParams = ConnectParamsParser  | 
|                 .Parse(args)  | 
|                 .GetValues<ConnectParams>(TokenId.ConnectParams)  | 
|                 .FirstOrDefault();  | 
|   | 
|             if (connParams == null)  | 
|                 return false;  | 
|   | 
|             portFrom = connParams.Port;  | 
|             if (connParams.MaxPort.HasValue)  | 
|                 portTo = connParams.MaxPort.Value;  | 
|             hostName = connParams.Host;  | 
|             fileName = connParams.File;  | 
|             block = connParams.Block;  | 
|   | 
|             return true;  | 
|         }  | 
|     }  | 
| }  |