/**************************************************************************** ** ** Copyright (C) 2019 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.CodeDom.Compiler; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Automation; using Microsoft.CSharp; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; using Task = System.Threading.Tasks.Task; namespace QtVsTest.Macros { /// /// Macros are snippets of C# code provided by a test client at runtime. They are compiled /// on-the-fly and may run once after compilation or stored and reused later by other macros. /// Macros may also include special statements in comment lines starting with '//#'. These will /// be expanded into the corresponding code ahead of C# compilation. /// class Macro { /// /// Global variable, shared between macros /// class GlobalVar { public string Name { get; set; } public string Type { get; set; } public string InitialValueExpr { get; set; } public FieldInfo FieldInfo { get; set; } public PropertyInfo InitInfo { get; set; } public bool IsCallOutput { get; set; } } /// /// Reference to Visual Studio SDK service /// class VSServiceRef { public string Name { get; set; } public string Interface { get; set; } public string Type { get; set; } public FieldInfo RefVar { get; set; } public Type ServiceType { get; set; } } /// /// Name of reusable macro /// private string Name { get; set; } /// /// True if macro compilation was successful /// public bool Ok { get; private set; } /// /// Result of macro compilation and execution /// public string Result { get; private set; } /// /// True if macro will run immediately after compilation /// public bool AutoRun { get; private set; } /// /// True if Visual Studio should be closed after macro execution /// public bool QuitWhenDone { get; private set; } AsyncPackage Package { get; } EnvDTE80.DTE2 Dte { get; } IntPtr MainWindowHWnd { get; } AutomationElement UiRoot => AutomationElement.RootElement; AutomationElement _UiVsRoot; AutomationElement UiVsRoot { get { if (_UiVsRoot == null) _UiVsRoot = AutomationElement.FromHandle(MainWindowHWnd); return _UiVsRoot; } } JoinableTaskFactory JoinableTaskFactory { get; } CancellationToken ServerLoop { get; } string Message { get; set; } static MacroParser Parser { get; set; } MacroLines MacroLines { get; set; } private List SelectedAssemblies { get; } = new List { typeof(Macro).Assembly.FullName, typeof(EnvDTE.DTE).Assembly.FullName, typeof(AutomationElement).Assembly.FullName, "System.Core", }; IEnumerable RefAssemblies { get; set; } private List Namespaces { get; } = new List { "System", "System.Linq", "System.Reflection", "Task = System.Threading.Tasks.Task", "System.Windows.Automation", "EnvDTE", "EnvDTE80", }; private Dictionary ServiceRefs { get; } = new Dictionary { { "Dte", new VSServiceRef { Name = "Dte", Interface = "DTE2", Type = "DTE" } }, }; private Dictionary GlobalVars { get; } = new Dictionary { { "Result", new GlobalVar { Type = "string", Name = "Result", InitialValueExpr = "string.Empty" } }, }; string CSharpMethodCode { get; set; } string CSharpClassCode { get; set; } CompilerResults CompilerResults { get; set; } Assembly MacroAssembly { get; set; } Type MacroClass { get; set; } FieldInfo ResultField { get; set; } Func Run { get; set; } const BindingFlags PUBLIC_STATIC = BindingFlags.Public | BindingFlags.Static; const StringComparison IGNORE_CASE = StringComparison.InvariantCultureIgnoreCase; static readonly ConcurrentDictionary Macros = new ConcurrentDictionary(); static readonly ConcurrentDictionary Globals = new ConcurrentDictionary(); /// /// Macro constructor /// /// QtVSTest extension package /// Task factory, enables joining with UI thread /// Server loop cancellation token public Macro( AsyncPackage package, EnvDTE80.DTE2 dte, IntPtr mainWindowHWnd, JoinableTaskFactory joinableTaskFactory, CancellationToken serverLoop) { Package = package; JoinableTaskFactory = joinableTaskFactory; ServerLoop = serverLoop; Dte = dte; MainWindowHWnd = mainWindowHWnd; ErrorMsg("Uninitialized"); } /// /// Compile macro code /// /// Message from client containing macro code public async Task CompileAsync(string msg) { if (MacroLines != null) return Warning("Macro already compiled"); try { Message = msg; if (!ParseMessage()) return false; if (!CompileMacro()) return false; if (!CompileClass()) return false; await GetServicesAsync(); return true; } catch (Exception e) { return ErrorException(e); } } /// /// Run macro /// public async Task RunAsync() { if (!Ok) return; try { InitGlobals(); await Run(); await SwitchToWorkerThreadAsync(); Result = ResultField.GetValue(null) as string; if (string.IsNullOrEmpty(Result)) Result = MACRO_OK; } catch (Exception e) { ErrorException(e); } UpdateGlobals(); } /// /// Parse message text into sequence of macro statements /// /// bool ParseMessage() { if (Parser == null) { var parser = MacroParser.Get(); if (parser == null) return ErrorMsg("Parser error"); Parser = parser; } var macroLines = Parser.Parse(Message); if (macroLines == null) return ErrorMsg("Parse error"); MacroLines = macroLines; return NoError(); } /// /// Expand macro statements into C# code /// /// bool CompileMacro() { if (UiVsRoot == null) return ErrorMsg("UI Automation not available"); var csharp = new StringBuilder(); foreach (var line in MacroLines) { if (QuitWhenDone) return ErrorMsg("No code allowed after #quit"); if (line is CodeLine) { var codeLine = line as CodeLine; csharp.Append(codeLine.Code + "\r\n"); continue; } if (!GenerateStatement(line as Statement, csharp)) return false; } if (csharp.Length > 0) CSharpMethodCode = csharp.ToString(); AutoRun = string.IsNullOrEmpty(Name); if (AutoRun) Name = "Macro_" + Path.GetRandomFileName().Replace(".", ""); else if (!SaveMacro(Name)) return ErrorMsg("Macro already defined"); foreach (var sv in ServiceRefs.Values.Where(x => string.IsNullOrEmpty(x.Type))) sv.Type = sv.Interface; var selectedAssemblyNames = SelectedAssemblies .Select(x => new AssemblyName(x)) .GroupBy(x => x.FullName) .Select(x => x.First()); var allAssemblies = AppDomain.CurrentDomain.GetAssemblies() .GroupBy(x => x.GetName().Name) .ToDictionary(x => x.Key, x => x.AsEnumerable(), StringComparer.InvariantCultureIgnoreCase); var refAssemblies = selectedAssemblyNames .GroupBy(x => allAssemblies.ContainsKey(x.Name)) .SelectMany(x => x.Key ? x.SelectMany(y => allAssemblies[y.Name]) : x.Select(y => { try { return Assembly.Load(y); } catch { return null; } })); RefAssemblies = refAssemblies .Where(x => x != null) .Select(x => x.Location); return NoError(); } bool GenerateStatement(Statement s, StringBuilder csharp) { switch (s.Type) { case StatementType.Quit: QuitWhenDone = true; break; case StatementType.Macro: if (csharp.Length > 0) return ErrorMsg("#macro must be first statement"); if (!string.IsNullOrEmpty(Name)) return ErrorMsg("Only one #macro statement allowed"); if (s.Args.Count < 1) return ErrorMsg("Missing macro name"); Name = s.Args[0]; break; case StatementType.Thread: if (s.Args.Count < 1) return ErrorMsg("Missing thread id"); if (s.Args[0].Equals("ui", IGNORE_CASE)) { csharp.Append( /** BEGIN generate code **/ @" await SwitchToUIThread();" /** END generate code **/ ); } else if (s.Args[0].Equals("default", IGNORE_CASE)) { csharp.Append( /** BEGIN generate code **/ @" await SwitchToWorkerThread();" /** END generate code **/ ); } else { return ErrorMsg("Unknown thread id"); } break; case StatementType.Reference: if (!s.Args.Any()) return ErrorMsg("Missing args for #reference"); SelectedAssemblies.Add(s.Args.First()); foreach (var ns in s.Args.Skip(1)) Namespaces.Add(ns); break; case StatementType.Using: if (!s.Args.Any()) return ErrorMsg("Missing args for #using"); foreach (var ns in s.Args) Namespaces.Add(ns); break; case StatementType.Var: if (s.Args.Count < 1) return ErrorMsg("Missing args for #var"); string typeName, varName; if (s.Args.Count == 1) { typeName = "object"; varName = s.Args[0]; } else { typeName = s.Args[0]; varName = s.Args[1]; } var initValue = s.Code; if (varName.Where(c => char.IsWhiteSpace(c)).Any()) return ErrorMsg("Wrong var name"); GlobalVars[varName] = new GlobalVar { Type = typeName, Name = varName, InitialValueExpr = initValue }; break; case StatementType.Service: if (s.Args.Count <= 1) return ErrorMsg("Missing args for #service"); var serviceVarName = s.Args[0]; if (serviceVarName.Where(c => char.IsWhiteSpace(c)).Any()) return ErrorMsg("Invalid service var name"); if (ServiceRefs.ContainsKey(serviceVarName)) return ErrorMsg("Duplicate service var name"); ServiceRefs.Add(serviceVarName, new VSServiceRef { Name = serviceVarName, Interface = s.Args[1], Type = s.Args.Count > 2 ? s.Args[2] : s.Args[1] }); break; case StatementType.Call: if (s.Args.Count < 1) return ErrorMsg("Missing args for #call"); var calleeName = s.Args[0]; var callee = GetMacro(calleeName); if (callee == null) return ErrorMsg("Undefined macro"); csharp.AppendFormat( /** BEGIN generate code **/ @" await CallMacro(""{0}"");" /** END generate code **/ , calleeName); foreach (var globalVar in callee.GlobalVars.Values) { if (GlobalVars.ContainsKey(globalVar.Name)) continue; GlobalVars[globalVar.Name] = new GlobalVar { Type = globalVar.Type, Name = globalVar.Name, IsCallOutput = true }; } break; case StatementType.Wait: if (string.IsNullOrEmpty(s.Code)) return ErrorMsg("Missing args for #wait"); var expr = s.Code; uint timeout = uint.MaxValue; if (s.Args.Count > 0 && !uint.TryParse(s.Args[0], out timeout)) return ErrorMsg("Timeout format error in #wait"); if (s.Args.Count > 2) { var evalVarType = s.Args[1]; var evalVarName = s.Args[2]; csharp.AppendFormat( /** BEGIN generate code **/ @" {0} {1} = default({0}); await WaitExpr({2}, () => {1} = {3});" /** END generate code **/ , evalVarType, evalVarName, timeout, expr); } else { csharp.AppendFormat( /** BEGIN generate code **/ @" await WaitExpr({0}, () => {1});" /** END generate code **/ , timeout, expr); } break; case StatementType.Ui: if (!GenerateUiStatement(s, csharp)) return false; break; } csharp.AppendLine(); return true; } public AutomationElement UiFind(AutomationElement uiContext, object[] path) { var uiIterator = uiContext; foreach (var item in path) { var itemType = item.GetType(); var scope = (uiIterator == UiRoot) ? TreeScope.Children : TreeScope.Subtree; if (itemType.IsAssignableFrom(typeof(string))) { // Find element by name var name = (string)item; uiIterator = uiIterator.FindFirst(scope, new PropertyCondition(AutomationElement.NameProperty, name)); } else if (itemType.IsAssignableFrom(typeof(string[]))) { // Find element by name and type var itemParams = (string[])item; uiIterator = uiIterator.FindFirst(scope, new AndCondition(itemParams.Select((x, i) => (i == 0) ? new PropertyCondition( AutomationElement.NameProperty, x) : (i == 1) ? new PropertyCondition( AutomationElement.LocalizedControlTypeProperty, x) : (i == 2) ? new PropertyCondition( AutomationElement.AutomationIdProperty, x) : Condition.FalseCondition).ToArray())); } if (uiIterator == null) throw new Exception( string.Format("Could not find UI element \"{0}\"", item)); } return uiIterator; } static readonly IEnumerable UI_TYPES = new[] { "Dock", "ExpandCollapse", "GridItem", "Grid", "Invoke", "MultipleView", "RangeValue", "Scroll", "ScrollItem", "Selection", "SelectionItem", "SynchronizedInput", "Text", "Transform", "Toggle", "Value", "Window", "VirtualizedItem", "ItemContainer" }; bool GenerateUiGlobals(StringBuilder csharp) { csharp.Append(@" public static Func UiFind; public static AutomationElement UiRoot; public static AutomationElement UiVsRoot; public static AutomationElement UiContext;"); return true; } bool InitializeUiGlobals() { if (MacroClass == null) return false; MacroClass.GetField("UiFind", PUBLIC_STATIC) .SetValue(null, new Func(UiFind)); MacroClass.GetField("UiRoot", PUBLIC_STATIC) .SetValue(null, UiRoot); MacroClass.GetField("UiVsRoot", PUBLIC_STATIC) .SetValue(null, UiVsRoot); MacroClass.GetField("UiContext", PUBLIC_STATIC) .SetValue(null, UiVsRoot); return true; } bool GenerateUiStatement(Statement s, StringBuilder csharp) { if (s.Args.Count == 0) return ErrorMsg("Invalid #ui statement"); if (s.Args[0].Equals("context", IGNORE_CASE)) { //# ui context [ VSROOT | DESKTOP ] [_int_] => _string_ [, _string_, ... ] //# ui context HWND [_int_] => _int_ if (s.Args.Count > 3 || string.IsNullOrEmpty(s.Code)) return ErrorMsg("Invalid #ui statement"); bool uiVsRoot = (s.Args.Count > 1 && s.Args[1] == "VSROOT"); bool uiDesktop = (s.Args.Count > 1 && s.Args[1] == "DESKTOP"); bool uiHwnd = (s.Args.Count > 1 && s.Args[1] == "HWND"); string context; if (uiVsRoot) context = string.Format("UiFind(UiVsRoot, new object[] {{ {0} }})", s.Code); else if (uiDesktop) context = string.Format("UiFind(UiRoot, new object[] {{ {0} }})", s.Code); else if (uiHwnd) context = string.Format("AutomationElement.FromHandle((IntPtr)({0}))", s.Code); else context = string.Format("UiFind(UiContext, new object[] {{ {0} }})", s.Code); int timeout = 3000; if (s.Args.Count > 1 && !uiVsRoot && !uiDesktop && !uiHwnd) timeout = int.Parse(s.Args[1]); else if (s.Args.Count > 2) timeout = int.Parse(s.Args[2]); csharp.AppendFormat(@" await WaitExpr({0}, () => UiContext = {1});", timeout, context); } else if (s.Args[0].Equals("find", IGNORE_CASE)) { //# ui find [all] [_var_name_] [_timeout_] => <_scope_>, <_condition_> var args = new Queue(s.Args.Skip(1)); bool findAll = false; if (args.Any() && args.Peek().Equals("all", IGNORE_CASE)) { findAll = true; args.Dequeue(); } string funcName = findAll ? "FindAll" : "FindFirst"; string varType = findAll ? "AutomationElementCollection" : "AutomationElement"; string varName = null; if (args.Any() && !char.IsDigit(args.Peek()[0])) varName = args.Dequeue(); if (findAll && string.IsNullOrEmpty(varName)) return ErrorMsg("Invalid #ui statement"); int timeout = 3000; if (args.Any() && char.IsDigit(args.Peek()[0])) timeout = int.Parse(args.Dequeue()); if (varName == null) { varName = "UiContext"; } else { csharp.Append($@" {varType} {varName} = null;"); } csharp.Append($@" await WaitExpr({timeout}, () => {varName} = UiContext.{funcName}({s.Code}));"); } else if (s.Args[0].Equals("pattern", IGNORE_CASE)) { //# ui pattern <_TypeName_> <_VarName_> [ => _string_ [, _string_, ... ] ] //# ui pattern Invoke [ => _string_ [, _string_, ... ] ] //# ui pattern Toggle [ => _string_ [, _string_, ... ] ] if (s.Args.Count < 2) return ErrorMsg("Invalid #ui statement"); string typeName = s.Args[1]; string varName = (s.Args.Count > 2) ? s.Args[2] : string.Empty; if (!UI_TYPES.Contains(typeName)) return ErrorMsg("Invalid #ui statement"); string uiElement; if (!string.IsNullOrEmpty(s.Code)) uiElement = string.Format("UiFind(UiContext, new object[] {{ {0} }})", s.Code); else uiElement = "UiContext"; string patternTypeId = string.Format("{0}PatternIdentifiers.Pattern", typeName); string patternType = string.Format("{0}Pattern", typeName); if (!string.IsNullOrEmpty(varName)) { csharp.AppendFormat(@" var {0} = {1}.GetCurrentPattern({2}) as {3};", varName, uiElement, patternTypeId, patternType); } else if (typeName == "Invoke" || typeName == "Toggle") { csharp.AppendFormat(@" ({0}.GetCurrentPattern({1}) as {2}).{3}();", uiElement, patternTypeId, patternType, typeName); } else { return ErrorMsg("Invalid #ui statement"); } } else { return ErrorMsg("Invalid #ui statement"); } return true; } const string SERVICETYPE_PREFIX = "_ServiceType_"; const string INIT_PREFIX = "_Init_"; string MethodName { get { return string.Format("_Run_{0}_Async", Name); } } bool GenerateClass() { var csharp = new StringBuilder(); foreach (var ns in Namespaces) { csharp.AppendFormat( /** BEGIN generate code **/ @" using {0};" /** END generate code **/ , ns); } csharp.AppendFormat( /** BEGIN generate code **/ @" namespace QtVsTest.Macros {{ public class {0} {{" /** END generate code **/ , Name); foreach (var serviceRef in ServiceRefs.Values) { csharp.AppendFormat( /** BEGIN generate code **/ @" public static {2} {1}; public static readonly Type {0}{1} = typeof({3});" /** END generate code **/ , SERVICETYPE_PREFIX, serviceRef.Name, serviceRef.Interface, serviceRef.Type); } foreach (var globalVar in GlobalVars.Values) { csharp.AppendFormat( /** BEGIN generate code **/ @" public static {1} {2}; public static {1} {0}{2} {{ get {{ return ({3}); }} }}" /** END generate code **/ , INIT_PREFIX, globalVar.Type, globalVar.Name, !string.IsNullOrEmpty(globalVar.InitialValueExpr) ? globalVar.InitialValueExpr : string.Format("default({0})", globalVar.Type)); } csharp.Append( /** BEGIN generate code **/ @" static string MACRO_OK { get { return ""(ok)""; } } static string MACRO_ERROR { get { return ""(error)""; } } static string MACRO_WARN { get { return ""(warn)""; } } static string MACRO_ERROR_MSG(string msg) { return string.Format(""{0}\r\n{1}"", MACRO_ERROR, msg); } static string MACRO_WARN_MSG(string msg) { return string.Format(""{0}\r\n{1}"", MACRO_WARN, msg); } public static Func GetAssembly; public static Func SwitchToUIThread; public static Func SwitchToWorkerThread; public static Func CallMacro; public static Func, Task> WaitExpr;" /** END generate code **/ ); if (!GenerateResultFuncs(csharp)) return false; if (!GenerateUiGlobals(csharp)) return false; csharp.AppendFormat( /** BEGIN generate code **/ @" public static async Task {0}() {{ {1} }} }} /*class*/ }} /*namespace*/" /** END generate code **/ , MethodName, CSharpMethodCode); CSharpClassCode = csharp.ToString(); return true; } /// /// Generate and compile C# class for macro /// /// bool CompileClass() { if (!GenerateClass()) return false; var dllUri = new Uri(Assembly.GetExecutingAssembly().EscapedCodeBase); var dllPath = Uri.UnescapeDataString(dllUri.AbsolutePath); var macroDllPath = Path.Combine(Path.GetDirectoryName(dllPath), Name + ".dll"); if (File.Exists(macroDllPath)) File.Delete(macroDllPath); var cscParams = new CompilerParameters() { GenerateInMemory = false, OutputAssembly = macroDllPath }; cscParams.ReferencedAssemblies.AddRange(RefAssemblies.ToArray()); var cSharpProvider = new CSharpCodeProvider(); CompilerResults = cSharpProvider.CompileAssemblyFromSource(cscParams, CSharpClassCode); if (CompilerResults.Errors.Count > 0) { if (File.Exists(macroDllPath)) File.Delete(macroDllPath); return ErrorMsg(string.Join("\r\n", CompilerResults.Errors.Cast() .Select(x => $"{x.Line}: {x.ErrorText}") .Append(CSharpClassCode) .Union(RefAssemblies))); } MacroAssembly = AppDomain.CurrentDomain.Load(File.ReadAllBytes(macroDllPath)); MacroClass = MacroAssembly.GetType(string.Format("QtVsTest.Macros.{0}", Name)); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; if (File.Exists(macroDllPath)) File.Delete(macroDllPath); foreach (var serviceVar in ServiceRefs.Values) { serviceVar.RefVar = MacroClass.GetField(serviceVar.Name, PUBLIC_STATIC); var serviceType = MacroClass.GetField(SERVICETYPE_PREFIX + serviceVar.Name, PUBLIC_STATIC); serviceVar.ServiceType = (Type)serviceType.GetValue(null); } ResultField = MacroClass.GetField("Result", PUBLIC_STATIC); foreach (var globalVar in GlobalVars.Values) { globalVar.FieldInfo = MacroClass.GetField(globalVar.Name, PUBLIC_STATIC); if (!globalVar.IsCallOutput) { globalVar.InitInfo = MacroClass .GetProperty(INIT_PREFIX + globalVar.Name, PUBLIC_STATIC); } } Run = (Func)Delegate.CreateDelegate(typeof(Func), MacroClass.GetMethod(MethodName, PUBLIC_STATIC)); MacroClass.GetField("GetAssembly", PUBLIC_STATIC) .SetValue(null, new Func(GetAssembly)); MacroClass.GetField("SwitchToUIThread", PUBLIC_STATIC) .SetValue(null, new Func(SwitchToUIThreadAsync)); MacroClass.GetField("SwitchToWorkerThread", PUBLIC_STATIC) .SetValue(null, new Func(SwitchToWorkerThreadAsync)); MacroClass.GetField("CallMacro", PUBLIC_STATIC) .SetValue(null, new Func(CallMacroAsync)); MacroClass.GetField("WaitExpr", PUBLIC_STATIC) .SetValue(null, new Func, Task>(WaitExprAsync)); if (!InitializeUiGlobals()) return false; return NoError(); } Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { if (args.RequestingAssembly == null || args.RequestingAssembly != MacroAssembly) return null; var fullName = new AssemblyName(args.Name); var assemblyPath = RefAssemblies .Where(x => Path.GetFileNameWithoutExtension(x).Equals(fullName.Name, IGNORE_CASE)) .FirstOrDefault(); if (string.IsNullOrEmpty(assemblyPath)) return null; if (!File.Exists(assemblyPath)) return null; return Assembly.LoadFrom(assemblyPath); } public static Assembly GetAssembly(string name) { return AppDomain.CurrentDomain.GetAssemblies() .Where(x => x.GetName().Name == name) .FirstOrDefault(); } public async Task SwitchToUIThreadAsync() { await JoinableTaskFactory.SwitchToMainThreadAsync(ServerLoop); } public async Task SwitchToWorkerThreadAsync() { await TaskScheduler.Default; } public async Task CallMacroAsync(string macroName) { var callee = GetMacro(macroName); if (callee == null) throw new FileNotFoundException("Unknown macro"); callee.InitGlobals(); await callee.Run(); callee.UpdateGlobals(); // Refresh caller local copies of globals InitGlobals(); } public async Task WaitExprAsync(int timeout, Func expr) { var tMax = TimeSpan.FromMilliseconds(timeout); var tRemaining = tMax; var t = Stopwatch.StartNew(); object value; try { value = await Task.Run(() => expr()).WithTimeout(tRemaining); } catch { value = null; } bool ok = !IsDefaultValue(value); while (!ok && (tRemaining = (tMax - t.Elapsed)) > TimeSpan.Zero) { await Task.Delay(10); try { value = await Task.Run(() => expr()).WithTimeout(tRemaining); } catch { value = null; } ok = !IsDefaultValue(value); } if (!ok) throw new TimeoutException(); } bool IsDefaultValue(object obj) { if (obj == null) return true; else if (obj.GetType().IsValueType) return obj.Equals(Activator.CreateInstance(obj.GetType())); else return false; } void InitGlobals() { foreach (var globalVar in GlobalVars.Values) { string varName = globalVar.Name; Type varType = globalVar.FieldInfo.FieldType; if (Globals.TryGetValue(varName, out object value)) { Type valueType = value.GetType(); if (!varType.IsAssignableFrom(valueType)) { throw new InvalidCastException(string.Format( "Global variable '{0}': cannot assign '{1}' from '{2}'", varName, varType.Name, valueType.Name)); } globalVar.FieldInfo.SetValue(null, value); } else { globalVar.FieldInfo.SetValue(null, globalVar.InitInfo.GetValue(null)); } } } void UpdateGlobals() { foreach (var globalVar in GlobalVars.Values) { object value = globalVar.FieldInfo.GetValue(null); Globals.AddOrUpdate(globalVar.Name, value, (key, oldValue) => value); } } async Task GetServicesAsync() { foreach (var serviceRef in ServiceRefs.Values.Where(x => x.RefVar != null)) { serviceRef.RefVar.SetValue(null, await Package.GetServiceAsync(serviceRef.ServiceType)); } return await Task.FromResult(NoError()); } bool SaveMacro(string name) { if (Macros.ContainsKey(name)) return false; return Macros.TryAdd(Name = name, this); } static Macro GetMacro(string name) { if (!Macros.TryGetValue(name, out Macro macro)) return null; return macro; } public static void Reset() { Macros.Clear(); Globals.Clear(); } bool GenerateResultFuncs(StringBuilder csharp) { csharp.Append( /** BEGIN generate code **/ @" public static string Ok; public static string Error; public static Func ErrorMsg;" /** END generate code **/ ); return true; } bool InitializeResultFuncs() { if (MacroClass == null) return false; MacroClass.GetField("Ok", PUBLIC_STATIC) .SetValue(null, MACRO_OK); MacroClass.GetField("Error", PUBLIC_STATIC) .SetValue(null, MACRO_ERROR); MacroClass.GetField("ErrorMsg", PUBLIC_STATIC) .SetValue(null, new Func(MACRO_ERROR_MSG)); return true; } string MACRO_OK { get { return "(ok)"; } } string MACRO_ERROR { get { return "(error)"; } } string MACRO_WARN { get { return "(warn)"; } } string MACRO_ERROR_MSG(string msg) { return string.Format("{0}\r\n{1}", MACRO_ERROR, msg); } string MACRO_WARN_MSG(string msg) { return string.Format("{0}\r\n{1}", MACRO_WARN, msg); } bool NoError() { Result = MACRO_OK; return (Ok = true); } bool Error() { Result = MACRO_ERROR; return (Ok = false); } bool ErrorMsg(string errorMsg) { Result = MACRO_ERROR_MSG(errorMsg); return (Ok = false); } bool ErrorException(Exception e) { Result = MACRO_ERROR_MSG(string.Format("{0}\r\n\"{1}\"\r\n{2}", e.GetType().Name, e.Message, e.StackTrace)); return (Ok = false); } bool Warning(string warnMsg) { Result = MACRO_WARN_MSG(warnMsg); return (Ok = true); } } }