forked from canneverbe/Ketarin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
UserCSScript.cs
162 lines (142 loc) · 6.53 KB
/
UserCSScript.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
using System;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Windows.Forms;
using Microsoft.CSharp;
namespace Ketarin
{
public interface ICustomSetupScript
{
void Execute(IWin32Window owner, ApplicationJob app);
}
/// <summary>
/// Represents a C# script that can be compiled and executed.
/// </summary>
public class UserCSScript
{
/// <summary>
/// Custom scripts should not require namespace declarations or classes.
/// </summary>
private const string codeTemplate = @"
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Windows.Forms;
namespace Ketarin
{
public class CustomScript : Ketarin.ICustomSetupScript
{
public void Execute(IWin32Window owner, ApplicationJob app)
{
{0}
}
public void Abort(string error)
{
throw new ApplicationException(error);
}
}
}
";
#region Properties
/// <summary>
/// Gets or sets the CS-Code to execute.
/// </summary>
public string Code
{
get;
set;
}
/// <summary>
/// In order to understand compiler errors, this determines the line at which the user code starts.
/// </summary>
internal int LineAtCodeStart
{
get
{
string[] lines = codeTemplate.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
if (lines[i].Contains("{0}")) return i + 1;
}
return 0;
}
}
#endregion
/// <summary>
/// Creates a new instance of a C# script with given code.
/// </summary>
public UserCSScript(string code)
{
this.Code = code;
}
/// <summary>
/// Compiles the user code into an assembly.
/// </summary>
/// <param name="errors">Compiler errors if any</param>
public Assembly Compile(out CompilerErrorCollection errors)
{
// Create a code provider
// This class implements the 'CodeDomProvider' class as its base. All of the current .Net languages (at least Microsoft ones)
// come with thier own implemtation, thus you can allow the user to use the language of thier choice (though i recommend that
// you don't allow the use of c++, which is too volatile for scripting use - memory leaks anyone?)
CSharpCodeProvider csProvider = new CSharpCodeProvider();
// Setup our options
CompilerParameters options = new CompilerParameters
{
GenerateExecutable = false,
GenerateInMemory = true
};
// we want a Dll (or "Class Library" as its called in .Net)
// Saves us from deleting the Dll when we are done with it, though you could set this to false and save start-up time by next time by not having to re-compile
// And set any others you want, there a quite a few, take some time to look through them all and decide which fit your application best!
// Add any references you want the users to be able to access, be warned that giving them access to some classes can allow
// harmful code to be written and executed. I recommend that you write your own Class library that is the only reference it allows
// thus they can only do the things you want them to.
// (though things like "System.Xml.dll" can be useful, just need to provide a way users can read a file to pass in to it)
// Just to avoid bloatin this example to much, we will just add THIS program to its references, that way we don't need another
// project to store the interfaces that both this class and the other uses. Just remember, this will expose ALL public classes to
// the "script"
options.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);
options.ReferencedAssemblies.Add("System.Windows.Forms.dll");
options.ReferencedAssemblies.Add("System.dll");
options.ReferencedAssemblies.Add("System.Xml.dll");
// Compile our code
CompilerResults result = csProvider.CompileAssemblyFromSource(options, codeTemplate.Replace("{0}", this.Code));
errors = result.Errors;
return errors.HasErrors ? null : result.CompiledAssembly;
}
public void Execute(ApplicationJob argument)
{
CompilerErrorCollection errors;
Assembly assembly = Compile(out errors);
if (errors.HasErrors)
{
throw new ApplicationException("Script cannot be compiled: " + errors[0].ErrorText);
}
// Now that we have a compiled script, lets run them
foreach (Type type in assembly.GetExportedTypes())
{
foreach (Type iface in type.GetInterfaces())
{
if (iface != typeof(ICustomSetupScript)) continue;
// yay, we found a script interface, lets create it and run it!
// Get the constructor for the current type
// you can also specify what creation parameter types you want to pass to it,
// so you could possibly pass in data it might need, or a class that it can use to query the host application
ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
if (constructor != null && constructor.IsPublic)
{
// lets be friendly and only do things legitimitely by only using valid constructors
// we specified that we wanted a constructor that doesn't take parameters, so don't pass parameters
ICustomSetupScript scriptObject = constructor.Invoke(null) as ICustomSetupScript;
if (scriptObject != null)
{
scriptObject.Execute(null, argument);
}
}
}
}
}
}
}