-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathScript.cs
469 lines (377 loc) · 20 KB
/
Script.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
using ExcelScript.CommonUtilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using RoslynScripting.Internal;
using RoslynScripting.Internal.Marshalling;
using ScriptingAbstractions;
using ScriptingAbstractions.Factory;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting.Lifetime;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace RoslynScripting
{
internal class HostingHelper
{
public static readonly ClientSponsor ClientSponsor = new ClientSponsor(TimeSpan.FromMinutes(5));
private static int i = 0;
private int ThisId = ++i;
private static AppDomain _SharedScriptDomain = AppDomain.CreateDomain("SharedSandbox", AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation, AppDomain.CurrentDomain.PermissionSet);
private Lazy<AppDomain> _IndividualDomain;
public readonly AppDomain GlobalDomain;
public readonly AppDomain SharedScriptDomain;
public AppDomain IndividualScriptDomain
{
get
{
return _IndividualDomain.Value;
}
}
public HostingHelper()
{
this.GlobalDomain = AppDomain.CurrentDomain;
this.SharedScriptDomain = _SharedScriptDomain;
this._IndividualDomain = new Lazy<AppDomain>(CreateIndividualDomain);
}
private AppDomain CreateIndividualDomain()
{
var domain = AppDomain.CreateDomain("IndividualSandbox" + GetUniqueDomainId(), AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation, AppDomain.CurrentDomain.PermissionSet);
return domain;
}
public int GetUniqueDomainId()
{
return ThisId;
}
}
internal class ScriptRunnerCacheInfo<TGlobals>
where TGlobals : class, IScriptGlobals
{
public HostedScriptRunner<TGlobals> ScriptRunner { get; set; }
public AppDomain HostingDomain { get; set; }
}
public class Script<TGlobals> : IScript<TGlobals>, IXmlSerializable, IDisposable
where TGlobals : class, IScriptGlobals
{
/// <summary>
/// Null in case of void
/// </summary>
[HashValue]
public Type ReturnType { get; set; } = null;
[HashEnumerable]
public IList<IParameter> Parameters { get; protected set; } = new List<IParameter>();
[HashValue]
public string Code { get; set; } = String.Empty;
[HashValue]
public string Description { get; set; } = String.Empty;
public readonly Guid UniqueId = Guid.NewGuid(); // just to make debugging a little easier
[HashValue]
private ScriptingOptions m_ScriptingOptions { get; set; }
private readonly HostingHelper HostingHelper;
private readonly ClientSponsor clientSponsor = new ClientSponsor();
private ScriptRunnerCacheInfo<TGlobals> m_ScriptRunnerCacheInfo;
private IDisposable m_appDomainDisposable;
internal Func<AppDomain, IScriptGlobals> GlobalsFactory { private get; set; } // a factory that creates IScriptGlobals in the correct AppDomain
/// <summary>
/// Needed for XML Serialization
/// </summary>
private Script()
{
this.HostingHelper = new HostingHelper();
}
public Script(Func<AppDomain, IScriptGlobals> GlobalsFactory, ScriptingOptions ScriptingOptions)
: this()
{
this.GlobalsFactory = GlobalsFactory;
this.m_ScriptingOptions = ScriptingOptions;
}
public void Dispose()
{
TryUnregisterLease();
if(m_appDomainDisposable != null)
{
m_appDomainDisposable.Dispose();
m_appDomainDisposable = null;
}
}
public static Task<string> GetRtfFormattedCodeAsync(string code, ScriptingOptions ScriptingOptions)
{
return GetRtfFormattedCodeAsync(code, ScriptingOptions, FormatColorScheme.LightTheme);
}
public static async Task<string> GetRtfFormattedCodeAsync(string code, ScriptingOptions ScriptingOptions, FormatColorScheme ColorScheme)
{
var script = new Script<TGlobals>(x => null, ScriptingOptions);
script.Code = code;
script.m_ScriptingOptions = ScriptingOptions;
var hostedScriptRunner = script.GetOrCreateScriptRunner(Array.Empty<IParameter>(), script.Code, script.m_ScriptingOptions);
var task = RemoteTask.ClientComplete<string>(hostedScriptRunner.GetRtfFormattedCodeAsync(code, ScriptingOptions, ColorScheme), CancellationToken.None);
var result = await task;
return result;
}
public static Task<FormattedText> GetFormattedCodeAsync(string code, ScriptingOptions ScriptingOptions)
{
return GetFormattedCodeAsync(code, ScriptingOptions, FormatColorScheme.LightTheme);
}
public static async Task<FormattedText> GetFormattedCodeAsync(string code, ScriptingOptions ScriptingOptions, FormatColorScheme ColorScheme)
{
var script = new Script<TGlobals>(x => null, ScriptingOptions);
script.Code = code;
script.m_ScriptingOptions = ScriptingOptions;
var hostedScriptRunner = script.GetOrCreateScriptRunner(Array.Empty<IParameter>(), script.Code, script.m_ScriptingOptions);
var task = RemoteTask.ClientComplete<FormattedText>(hostedScriptRunner.GetFormattedCodeAsync(code, ScriptingOptions, ColorScheme), CancellationToken.None);
var result = await task;
return result;
}
internal static async Task<IParseResult<TGlobals>> ParseFromAsync(Func<AppDomain, IScriptGlobals> GlobalsFactory, ScriptingOptions ScriptingOptions, string code, Func<MethodInfo[], MethodInfo> EntryMethodSelector, Func<MethodInfo, IParameter[]> EntryMethodParameterFactory)
{
var script = new Script<TGlobals>(GlobalsFactory, ScriptingOptions);
script.Code = code;
script.m_ScriptingOptions = ScriptingOptions;
// TODO: This is not a great solution.. can we improve this?
// Ideal would be to avoid double-parsing as to not garbage up the appdomain with lot of unneeded assemblies (or having to create individual ones like below)
// but then again we can't use the EntryMethodSelector that nicely anymore
// maybe only compile to semanticmodel and work with that?
var helper = new HostingHelper();
AppDomain domain = null;
ParseResult<TGlobals> result;
try
{
domain = helper.IndividualScriptDomain;
var scriptRunner = (HostedScriptRunner<TGlobals>)Activator.CreateInstance(domain, typeof(HostedScriptRunner<TGlobals>).Assembly.FullName, typeof(HostedScriptRunner<TGlobals>).FullName).Unwrap();
var task = RemoteTask.ClientComplete<RoslynScripting.Internal.IParseResult>(scriptRunner.ParseAsync(script.Code, script.m_ScriptingOptions, EntryMethodSelector, EntryMethodParameterFactory), CancellationToken.None);
var parseResult = await task;
IList<IParameter> marshalledParameters = new List<IParameter>();
foreach (var parameter in parseResult.Parameters)
marshalledParameters.Add(new Parameter { DefaultValue = parameter.DefaultValue, Description = parameter.Description, IsOptional = parameter.IsOptional, Name = parameter.Name, Type = parameter.Type });
script.Code = parseResult.RefactoredCode;
script.Parameters = marshalledParameters;
result = new ParseResult<TGlobals>(script, parseResult.EntryMethodName);
} finally
{
if(domain != null)
AppDomain.Unload(domain);
}
return result;
/*
var hostedScriptRunner = script.GetOrCreateScriptRunner(new IParameter[0], script.Code, script.m_ScriptingOptions);
var task = RemoteTask.ClientComplete<RoslynScripting.Internal.IParseResult>(hostedScriptRunner.ParseAsync(script.Code, script.m_ScriptingOptions, EntryMethodSelector, EntryMethodParameterFactory), CancellationToken.None);
var parseResult = await task;
script.Code = parseResult.RefactoredCode;
script.Parameters = parseResult.Parameters.ToList();
var result = new ParseResult<TGlobals>(script, parseResult.EntryMethodName);
return result;*/
}
public IScriptRunResult Run(IEnumerable<IParameterValue> Parameters)
{
return RunAsync(Parameters).Result;
}
/// <exception cref="ArgumentNullException">Thrown if Parameters is null</exception>
public async Task<IScriptRunResult> RunAsync(IEnumerable<IParameterValue> Parameters)
{
if (Parameters == null)
throw new ArgumentNullException("Parameters");
try
{
Parameters = CompleteParameterValues(Parameters);
}
catch (AggregateException ex)
{
return ScriptRunResult.Failure(ex);
}
var result = await InternalRunAsync(Parameters);
Type resultType = result?.GetType();
if (ReturnType == null)
return ScriptRunResult.Success();
if (!AreCompatible(ReturnType, resultType))
return ScriptRunResult.Failure(new InvalidCastException($"Expected return type of script to be {ReturnType.Name}, but was {resultType.Name}"));
return ScriptRunResult.Success(result);
}
/// <exception cref="ArgumentNullException">Thrown if Parameters is null</exception>
private async Task<object> InternalRunAsync(IEnumerable<IParameterValue> Parameters)
{
if (Parameters == null)
throw new ArgumentNullException("Parameters");
var hostedScriptRunner = GetOrCreateScriptRunner(Parameters.Select(x => x.Parameter).ToArray(), this.Code, this.m_ScriptingOptions);
var task = RemoteTask.ClientComplete<object>(hostedScriptRunner.RunAsync(this.GlobalsFactory, Parameters.ToArray(), this.Code, this.m_ScriptingOptions), CancellationToken.None);
var result = await task;
return result;
}
private IScriptRunner GetOrCreateScriptRunner(IParameter[] parameters, string scriptCode, ScriptingOptions Options)
{
var hostingType = (Options == null) ? HostingType.SharedSandboxAppDomain : Options.HostingType;
AppDomain sandbox = GetOrCreateHostingDomain(hostingType);
// We can return a cached script runner if
// A) there is one (duh...), AND
// B) It does not need to recompile for the given input arguments, AND
// C) the AppDomain in which the script runner is hosted is the same that we would like to use now
if (m_ScriptRunnerCacheInfo != null && !m_ScriptRunnerCacheInfo.ScriptRunner.NeedsRecompilationFor(parameters, scriptCode, Options) && m_ScriptRunnerCacheInfo.HostingDomain == sandbox)
{
Trace.WriteLine("Using cached script runner");
return m_ScriptRunnerCacheInfo.ScriptRunner;
}
else
{
Trace.WriteLine("Creating new script runner");
TryUnregisterLease();
var scriptRunner = (HostedScriptRunner<TGlobals>)Activator.CreateInstance(sandbox, typeof(HostedScriptRunner<TGlobals>).Assembly.FullName, typeof(HostedScriptRunner<TGlobals>).FullName).Unwrap();
HostingHelper.ClientSponsor.Register(scriptRunner);
this.m_ScriptRunnerCacheInfo = new ScriptRunnerCacheInfo<TGlobals> { HostingDomain = sandbox, ScriptRunner = scriptRunner };
return scriptRunner;
}
}
/// <summary>
/// Tries to unregister the lease of the cached script runner
/// </summary>
private void TryUnregisterLease()
{
if (m_ScriptRunnerCacheInfo != null && m_ScriptRunnerCacheInfo.ScriptRunner != null)
{
HostingHelper.ClientSponsor.Unregister(m_ScriptRunnerCacheInfo.ScriptRunner);
m_ScriptRunnerCacheInfo = null;
}
}
/// <summary>
/// Returns the AppDomain that shall be used to host script execution.
/// </summary>
/// <returns></returns>
private AppDomain GetOrCreateHostingDomain(HostingType hostingType) {
AppDomain domain;
switch(hostingType)
{
case HostingType.GlobalAppDomain:
Trace.WriteLine($"Script {UniqueId} - executing in global domain");
domain = this.HostingHelper.GlobalDomain;
this.m_appDomainDisposable = null;
return domain;
case HostingType.SharedSandboxAppDomain:
Trace.WriteLine($"Script {UniqueId} - executing in shared domain");
this.m_appDomainDisposable = null;
domain = this.HostingHelper.SharedScriptDomain;
return domain;
case HostingType.IndividualScriptAppDomain:
Trace.WriteLine($"Script {UniqueId} - executing in individual domain");
domain = this.HostingHelper.IndividualScriptDomain;
this.m_appDomainDisposable = new DelegateDisposable(() => DisposeDomain(domain));
return domain;
default:
throw new InvalidOperationException($"Unknown HostingType {hostingType}");
}
}
private void DisposeDomain(AppDomain domain)
{
Trace.WriteLine($"Script {UniqueId} - Unloading AppDomain {domain}");
AppDomain.Unload(domain);
}
/// <exception cref="ArgumentNullException">Thrown if expectedType or givenType is null</exception>
private static bool AreCompatible(Type expectedType, Type givenType)
{
if (expectedType == null)
throw new ArgumentNullException("expectedType");
if (!expectedType.IsValueType && givenType == null)
// expected type is a reference type and giventype = null, i.e. given type could be anything, including expectedtype - return true!
return true;
if (givenType == null)
throw new ArgumentNullException("givenType");
bool success = expectedType.IsAssignableFrom(givenType);
bool assemblyEqual = expectedType.Assembly.Equals(givenType.Assembly);
bool assemblyLocationEqual = expectedType.Assembly.Location.Equals(givenType.Assembly.Location);
if (expectedType.AssemblyQualifiedName == givenType.AssemblyQualifiedName && (!assemblyEqual || !assemblyLocationEqual))
throw new InvalidProgramException($"Types {expectedType.AssemblyQualifiedName} vs {givenType.AssemblyQualifiedName} appear to be loaded in different AppDomains");
return success;
}
/// <exception cref="AggregateException">Thrown if not all non-optional parameters were given, or GivenParameters contains any parameters that were not part of the script</exception>
/// <exception cref="ArgumentNullException">Thrown if GivenParameters is null</exception>
private IEnumerable<IParameterValue> CompleteParameterValues(IEnumerable<IParameterValue> GivenParameters)
{
if (GivenParameters == null)
throw new ArgumentNullException("GivenParameters");
var givenParametersDefinitions = GivenParameters.Select(x => x.Parameter);
var invalidParameters = givenParametersDefinitions.Except(Parameters);
if(invalidParameters.Any())
{
var exceptions = invalidParameters.Select(x => new ArgumentException(String.Format("Parameter '{0}' is not a parameter of the script", x.Name), x.Name));
throw new AggregateException("The given parameters contain parameters that are not aprt of the script definition", exceptions);
}
var mandatoryParameters = Parameters.Where(x => !x.IsOptional);
var missingMandatoryParameters = mandatoryParameters.Except(givenParametersDefinitions);
if(missingMandatoryParameters.Any())
{
var exceptions = missingMandatoryParameters.Select(x => new ArgumentException(String.Format("Parameter '{0}' was not given", x.Name), x.Name));
throw new AggregateException("Not all required parameters were given", exceptions);
}
var optionalParameters = Parameters.Where(x => x.IsOptional);
var missingOptionalParameters = optionalParameters.Except(givenParametersDefinitions);
var missingOptionalParameterValues = missingMandatoryParameters.Select(x => (IParameterValue)new ParameterValue(x, x.DefaultValue));
var allParameterValues = GivenParameters.Concat(missingOptionalParameterValues);
return allParameterValues;
}
public override int GetHashCode()
{
int hash = HashHelper.HashOfAnnotated<Script<TGlobals>>(this);
int domainId;
// when e.g. two scripts are hosted with the individual domain setting, and everything else is equal (code, parameters etc)
// we still don't want to return the same hash code, because they should be run in different domains.
// hence, we acount for that case here:
switch(this.m_ScriptingOptions.HostingType)
{
case HostingType.GlobalAppDomain:
domainId = -2;
break;
case HostingType.SharedSandboxAppDomain:
domainId = -3;
break;
case HostingType.IndividualScriptAppDomain:
domainId = this.HostingHelper.GetUniqueDomainId();
break;
default:
throw new InvalidOperationException($"Unknown hosting type {m_ScriptingOptions.HostingType}");
}
unchecked
{
hash = (hash * 7) ^ domainId.GetHashCode();
}
return hash;
}
#region IXmlSerializable
public XmlSchema GetSchema()
{
return null;
}
public void WriteXml(XmlWriter writer)
{
writer.Write(nameof(ReturnType), ReturnType);
writer.WriteEnumerable(nameof(Parameters), Parameters);
writer.Write(nameof(Code), Code);
writer.Write(nameof(Description), Description);
writer.Write(nameof(m_ScriptingOptions), m_ScriptingOptions);
}
public void ReadXml(XmlReader reader)
{
reader.MoveToCustomStart();
Type _ReturnType;
IEnumerable<IParameter> _Parameters;
string _Code;
string _Description;
object _ScriptingOptions;
reader.Read(nameof(ReturnType), out _ReturnType);
_Parameters = reader.ReadEnumerable(nameof(Parameters)).Cast<IParameter>();
reader.Read(nameof(Code), out _Code);
reader.Read(nameof(Description), out _Description);
reader.Read(nameof(m_ScriptingOptions), out _ScriptingOptions);
this.ReturnType = _ReturnType;
this.Parameters = _Parameters.ToList();
this.Code = _Code;
this.Description = _Description;
this.m_ScriptingOptions = (ScriptingOptions)_ScriptingOptions;
reader.ReadEndElement();
}
#endregion
}
}