2019-05-02 22:45:40 +00:00
|
|
|
|
/*
|
|
|
|
|
* Copyright 2019 faddenSoft
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
using System;
|
|
|
|
|
using System.CodeDom.Compiler;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
|
|
|
|
using CommonUtil;
|
|
|
|
|
using PluginCommon;
|
|
|
|
|
|
2019-07-20 20:28:10 +00:00
|
|
|
|
namespace SourceGen.Sandbox {
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// <summary>
|
2020-07-16 17:35:14 +00:00
|
|
|
|
/// This manages the PluginDllCache directory, which holds the compiled form of the extension
|
2019-05-02 22:45:40 +00:00
|
|
|
|
/// scripts. When a script is requested, this checks to see if the compiled form
|
|
|
|
|
/// already exists. If not, or the script source file is newer than the DLL file, the
|
|
|
|
|
/// compiler is executed.
|
|
|
|
|
///
|
|
|
|
|
/// This is global -- it's not tied to an active project.
|
|
|
|
|
///
|
|
|
|
|
/// If an assembly is still loaded, the file on disk will be locked by the operating
|
|
|
|
|
/// system and can't be replaced. So long as the plugins run in an AppDomain sandbox,
|
|
|
|
|
/// the locks will be cleared when the AppDomain is unloaded.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static class PluginDllCache {
|
2020-07-16 17:35:14 +00:00
|
|
|
|
private const string PLUGIN_DIR_NAME = "PluginDllCache";
|
2019-05-02 22:45:40 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// List of assemblies for the CompilerParameters.ReferencedAssemblies argument.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static readonly string[] sRefAssem = new string[] {
|
|
|
|
|
// Need this for various things to work, like System.Collections.Generic.
|
|
|
|
|
"netstandard.dll",
|
|
|
|
|
|
|
|
|
|
// Plugins are implemented in terms of interfaces defined here.
|
|
|
|
|
"PluginCommon.dll",
|
|
|
|
|
|
|
|
|
|
// Common utility functions.
|
|
|
|
|
"CommonUtil.dll",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Path to plugin directory.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static string sPluginDirPath;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Computes the path to the plugin directory. Does not attempt to verify that it exists.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>Plugin directory path, or null if we can't find the application data
|
|
|
|
|
/// area.</returns>
|
|
|
|
|
public static string GetPluginDirPath() {
|
|
|
|
|
if (sPluginDirPath == null) {
|
|
|
|
|
string runtimeUp = Path.GetDirectoryName(RuntimeDataAccess.GetDirectory());
|
|
|
|
|
if (runtimeUp == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
sPluginDirPath = Path.Combine(runtimeUp, PLUGIN_DIR_NAME);
|
|
|
|
|
}
|
|
|
|
|
return sPluginDirPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Prepares the plugin directory. Creates it and copies PluginCommon.dll in.
|
|
|
|
|
/// Throws an exception if something fails.
|
|
|
|
|
/// </summary>
|
2019-12-14 00:52:50 +00:00
|
|
|
|
/// <exception cref="IOException">Various failures from file/dir operations.</exception>
|
2019-05-02 22:45:40 +00:00
|
|
|
|
public static void PreparePluginDir() {
|
|
|
|
|
string dstDir = GetPluginDirPath();
|
|
|
|
|
if (File.Exists(dstDir) && !Directory.Exists(dstDir)) {
|
|
|
|
|
throw new IOException(
|
|
|
|
|
string.Format(Res.Strings.ERR_FILE_EXISTS_NOT_DIR_FMT, dstDir));
|
|
|
|
|
}
|
|
|
|
|
Directory.CreateDirectory(dstDir);
|
|
|
|
|
|
|
|
|
|
// TODO(someday): try to remove *.dll where the modification date is more than a
|
|
|
|
|
// week old -- this will prevent us from accreting stuff indefinitely.
|
|
|
|
|
|
|
|
|
|
// Copy PluginCommon and CommonUtil over.
|
|
|
|
|
CopyIfNewer(typeof(PluginCommon.PluginManager).Assembly.Location, dstDir);
|
|
|
|
|
CopyIfNewer(typeof(CommonUtil.CRC32).Assembly.Location, dstDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Copies a DLL file if it's not present in the destination directory, or
|
|
|
|
|
/// if it's newer than what's in the destination directory.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="srcDll">Full path to DLL file.</param>
|
|
|
|
|
/// <param name="dstDir">Destination directory.</param>
|
|
|
|
|
private static void CopyIfNewer(string srcDll, string dstDir) {
|
|
|
|
|
string dstFile = Path.Combine(dstDir, Path.GetFileName(srcDll));
|
2019-12-29 01:14:29 +00:00
|
|
|
|
if (FileUtil.IsFileMissingOrOlder(dstFile, srcDll)) {
|
2019-05-02 22:45:40 +00:00
|
|
|
|
Debug.WriteLine("Copying " + srcDll + " to " + dstFile);
|
|
|
|
|
File.Copy(srcDll, dstFile, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Should we copy the .pdb files too, if they exist? If they don't exist in
|
|
|
|
|
// the source directory, do we need to remove them from the destination directory?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Prepares the DLL for the specified script, compiling it if necessary.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="scriptIdent">Script identifier.</param>
|
|
|
|
|
/// <param name="projectPathName">Project file name, used for naming project-local
|
|
|
|
|
/// files. May be empty if the project hasn't been named yet (in which case
|
|
|
|
|
/// project-local files will cause a failure).</param>
|
|
|
|
|
/// <param name="report">Report with errors and warnings.</param>
|
|
|
|
|
/// <returns>Full path to DLL, or null if compilation failed.</returns>
|
|
|
|
|
public static string GenerateScriptDll(string scriptIdent, string projectPathName,
|
|
|
|
|
out FileLoadReport report) {
|
|
|
|
|
ExternalFile ef = ExternalFile.CreateFromIdent(scriptIdent);
|
|
|
|
|
if (ef == null) {
|
|
|
|
|
Debug.Assert(false);
|
|
|
|
|
report = new FileLoadReport("CreateFromIdent failed");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
string projectDir = string.Empty;
|
|
|
|
|
if (!string.IsNullOrEmpty(projectPathName)) {
|
|
|
|
|
projectDir = Path.GetDirectoryName(projectPathName);
|
|
|
|
|
}
|
|
|
|
|
string srcPathName = ef.GetPathName(projectDir);
|
|
|
|
|
|
|
|
|
|
// Fail if the source script doesn't exist. If a previously-compiled DLL is present
|
|
|
|
|
// we could just continue to use it, but that seems contrary to expectation, and
|
|
|
|
|
// means that you won't notice that your project is broken until you clear out
|
|
|
|
|
// the DLL directory.
|
|
|
|
|
if (!File.Exists(srcPathName)) {
|
|
|
|
|
report = new FileLoadReport(srcPathName);
|
|
|
|
|
report.Add(FileLoadItem.Type.Error,
|
|
|
|
|
string.Format(Res.Strings.ERR_FILE_NOT_FOUND_FMT, srcPathName));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string destFileName = ef.GenerateDllName(projectPathName);
|
|
|
|
|
string destPathName = Path.Combine(GetPluginDirPath(), destFileName);
|
|
|
|
|
|
2019-10-06 03:58:48 +00:00
|
|
|
|
// Compile if necessary. We do this if the source code is newer, or if the
|
|
|
|
|
// DLLs that plugins depend on have been updated. (We're checking the dates on
|
|
|
|
|
// the DLLs the app uses, not the copy the plugins use, but earlier we made sure
|
|
|
|
|
// that they were the same. This test doesn't handle the case where the DLLs
|
|
|
|
|
// get rolled back, but that's probably not interesting for us.)
|
2019-12-29 01:14:29 +00:00
|
|
|
|
bool needCompile = FileUtil.IsFileMissingOrOlder(destPathName, srcPathName) ||
|
|
|
|
|
FileUtil.IsFileMissingOrOlder(destPathName,
|
2019-10-06 03:58:48 +00:00
|
|
|
|
typeof(PluginCommon.PluginManager).Assembly.Location) ||
|
2019-12-29 01:14:29 +00:00
|
|
|
|
FileUtil.IsFileMissingOrOlder(destPathName,
|
2019-10-06 03:58:48 +00:00
|
|
|
|
typeof(CommonUtil.CRC32).Assembly.Location);
|
|
|
|
|
if (needCompile) {
|
2019-05-02 22:45:40 +00:00
|
|
|
|
Debug.WriteLine("Compiling " + srcPathName + " to " + destPathName);
|
|
|
|
|
Assembly asm = CompileCode(srcPathName, destPathName, out report);
|
|
|
|
|
if (asm == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Debug.WriteLine("NOT recompiling " + srcPathName);
|
|
|
|
|
report = new FileLoadReport(srcPathName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return destPathName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Compiles the script from the specified pathname into an Assembly.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="scriptPathName">Script pathname.</param>
|
|
|
|
|
/// <param name="dllPathName">Full pathname for output DLL.</param>
|
|
|
|
|
/// <param name="report">Errors and warnings reported by the compiler.</param>
|
|
|
|
|
/// <returns>Reference to script instance, or null on failure.</returns>
|
|
|
|
|
private static Assembly CompileCode(string scriptPathName, string dllPathName,
|
|
|
|
|
out FileLoadReport report) {
|
|
|
|
|
report = new FileLoadReport(scriptPathName);
|
|
|
|
|
|
2019-12-18 00:40:27 +00:00
|
|
|
|
// To get C#6 (and later) features, a NuGet package must be installed, and
|
|
|
|
|
// some "black magic" must be invoked.
|
|
|
|
|
// See https://stackoverflow.com/a/40311406/294248 and nearby answers.
|
|
|
|
|
|
2019-05-02 22:45:40 +00:00
|
|
|
|
Microsoft.CSharp.CSharpCodeProvider csProvider =
|
|
|
|
|
new Microsoft.CSharp.CSharpCodeProvider();
|
|
|
|
|
|
|
|
|
|
CompilerParameters parms = new CompilerParameters();
|
|
|
|
|
// We want a DLL, not an EXE.
|
|
|
|
|
parms.GenerateExecutable = false;
|
|
|
|
|
// Save to disk so other AppDomain can load it.
|
|
|
|
|
parms.GenerateInMemory = false;
|
|
|
|
|
// Be vocal about warnings.
|
|
|
|
|
parms.WarningLevel = 3;
|
|
|
|
|
// Optimization is nice.
|
|
|
|
|
parms.CompilerOptions = "/optimize";
|
|
|
|
|
// Output file name. Must be named appropriately so it can be found.
|
|
|
|
|
parms.OutputAssembly = dllPathName;
|
|
|
|
|
// Add dependencies.
|
|
|
|
|
parms.ReferencedAssemblies.AddRange(sRefAssem);
|
|
|
|
|
#if DEBUG
|
|
|
|
|
// This creates a .pdb file, which allows breakpoints to work.
|
|
|
|
|
parms.IncludeDebugInformation = true;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// Using the "from file" version has an advantage over the "from source"
|
|
|
|
|
// version in that the debugger can find the source file, so things like
|
|
|
|
|
// breakpoints work correctly.
|
|
|
|
|
CompilerResults cr = csProvider.CompileAssemblyFromFile(parms, scriptPathName);
|
|
|
|
|
CompilerErrorCollection cec = cr.Errors;
|
|
|
|
|
foreach (CompilerError ce in cr.Errors) {
|
|
|
|
|
report.Add(ce.Line, ce.Column,
|
|
|
|
|
ce.IsWarning ? FileLoadItem.Type.Warning : FileLoadItem.Type.Error,
|
|
|
|
|
ce.ErrorText);
|
|
|
|
|
}
|
|
|
|
|
if (cr.Errors.HasErrors) {
|
|
|
|
|
return null;
|
|
|
|
|
} else {
|
|
|
|
|
Debug.WriteLine("Compilation successful");
|
|
|
|
|
return cr.CompiledAssembly;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Finds the first concrete class that implements IPlugin, and
|
|
|
|
|
/// constructs an instance.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static IPlugin ConstructIPlugin(Assembly asm) {
|
|
|
|
|
foreach (Type type in asm.GetExportedTypes()) {
|
|
|
|
|
// Using a System.Linq extension method.
|
|
|
|
|
if (type.IsClass && !type.IsAbstract &&
|
|
|
|
|
type.GetInterfaces().Contains(typeof(IPlugin))) {
|
|
|
|
|
|
|
|
|
|
ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
|
|
|
|
|
IPlugin iplugin = (IPlugin)ctor.Invoke(null);
|
|
|
|
|
Debug.WriteLine("Created instance: " + iplugin);
|
|
|
|
|
return iplugin;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|