/****************************************************************************
**
** 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 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; }
}
///
/// 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
///
public string Name { get; private 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; set; }
JoinableTaskFactory JoinableTaskFactory { get; set; }
CancellationToken ServerLoop { get; set; }
string Message { get; set; }
static MacroParser Parser { get; set; }
MacroLines MacroLines { get; set; }
List SelectedAssemblies { get { return _SelectedAssemblies; } }
List _SelectedAssemblies =
new List(MSBuild.MetaInfo.QtVsTest.Reference)
{
"QtVsTest",
"System.Core",
};
IEnumerable RefAssemblies { get; set; }
List Namespaces { get { return _Namespaces; } }
List _Namespaces =
new List
{
"System",
"System.Linq",
"System.Reflection",
"Task = System.Threading.Tasks.Task",
"System.Windows.Automation",
"EnvDTE",
"EnvDTE80",
};
Dictionary ServiceRefs { get { return _ServiceRefs; } }
Dictionary _ServiceRefs =
new Dictionary
{
{
"Dte", new VSServiceRef
{ Name = "Dte", Interface = "DTE2", Type = "DTE" }
},
};
Dictionary GlobalVars { get { return _GlobalVars; } }
Dictionary _GlobalVars =
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 ConcurrentDictionary Macros
= new ConcurrentDictionary();
///
/// Macro constructor
///
/// QtVSTest extension package
/// Task factory, enables joining with UI thread
/// Server loop cancellation token
public Macro(
AsyncPackage package,
JoinableTaskFactory joinableTaskFactory,
CancellationToken serverLoop)
{
Package = package;
JoinableTaskFactory = joinableTaskFactory;
ServerLoop = serverLoop;
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 (string.IsNullOrEmpty(CSharpMethodCode))
return true;
if (!CompileClass())
return false;
await GetServicesAsync();
return true;
} catch (Exception e) {
return ErrorException(e);
}
}
///
/// Run macro
///
public async Task RunAsync()
{
if (!Ok)
return;
if (string.IsNullOrEmpty(CSharpMethodCode))
return;
try {
InitGlobalVars();
await Run();
await SwitchToWorkerThreadAsync();
Result = ResultField.GetValue(null) as string;
} catch (Exception e) {
ErrorException(e);
}
}
///
/// 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()
{
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 < 2)
return ErrorMsg("Missing args for #var");
var typeName = s.Args[0];
var 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
};
}
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;
}
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,
globalVar.InitialValueExpr);
}
csharp.Append(
/** BEGIN generate code **/
@"
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;
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.ErrorText)));
}
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);
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));
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.InitGlobalVars();
callee.CopyGlobalVarsFrom(this);
await callee.Run();
CopyGlobalVarsFrom(callee);
}
public async Task WaitExprAsync(int timeout, Func