1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-11-12 06:06:12 +00:00
6502bench/SourceGen/Sandbox/PluginDllCache.cs
Andy McFadden 89413d11e4 Add file concatenation tool
Select a list of files and save it.  File lengths and CRCs are
shown for reference.
2019-12-28 17:22:19 -08:00

251 lines
11 KiB
C#

/*
* 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;
namespace SourceGen.Sandbox {
/// <summary>
/// This manages the PluginDll directory, which holds the compiled form of the extension
/// 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 {
private const string PLUGIN_DIR_NAME = "PluginDll";
/// <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>
/// <exception cref="IOException">Various failures from file/dir operations.</exception>
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));
if (FileUtil.IsFileMissingOrOlder(dstFile, srcDll)) {
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);
// 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.)
bool needCompile = FileUtil.IsFileMissingOrOlder(destPathName, srcPathName) ||
FileUtil.IsFileMissingOrOlder(destPathName,
typeof(PluginCommon.PluginManager).Assembly.Location) ||
FileUtil.IsFileMissingOrOlder(destPathName,
typeof(CommonUtil.CRC32).Assembly.Location);
if (needCompile) {
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);
// 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.
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;
}
}
}