#nullable enable using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; public class MemorySourceText : SourceText { public override char this[int position] => ' '; public override Encoding? Encoding => Encoding.UTF8; public override int Length => 0; public override void CopyTo( int sourceIndex, char[] destination, int destinationIndex, int count ) { } } public class MemoryRefResolver : SourceReferenceResolver { public override bool Equals(object? other) { return false; } public override int GetHashCode() { return 0; } private MemoryStream _fakeMS = new MemoryStream(); public override string? NormalizePath( string? path, string? baseFilePath ) => path; public override Stream OpenRead( string resolvedPath ) => _fakeMS; private MemorySourceText _fakeMST = new(); public override SourceText ReadText( string resolvedPath ) => _fakeMST; public override string? ResolveReference( string? path, string? baseFilePath ) => path; } public static class scr { public static FieldInfo? GetFieldInfo(Type? t, string name) { if (t == null) return null; var fi = t.GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (fi != null) return fi; if (t.BaseType != null) return GetFieldInfo(t.BaseType, name); return null; } // From stack overflow static Lazy> typeSetLazy = new Lazy>(() => { var types = AppDomain .CurrentDomain .GetAssemblies() .SelectMany(a => a.GetTypes() .Where(t => t.IsClass)); var typesAndBaseTypes = types .Select(t => new { Type = t, t.BaseType }) .ToList(); var typesWithSubclasses = typesAndBaseTypes .Join( typesAndBaseTypes, t => t.Type, t => t.BaseType, (t1, t2) => t2.BaseType); var typesHs = new HashSet(types); typesHs.ExceptWith(typesWithSubclasses); return typesHs; }); static bool IsLeafType(this Type type) { return typeSetLazy.Value.Contains(type); } static HashSet s_badChars = new( new char[] { '<', '>', ' ', ',', '.', '+', '[', ']', '$', ':' } ); static public string TypeToIdentifier(string typename) { var safeStr = new StringBuilder( typename ); for( int i = 0; i < safeStr.Length; ++i ) { if( s_badChars.Contains(safeStr[i]) ) safeStr[i] = '_'; } return safeStr.ToString(); } static public FileSystemWatcher s_watcher = new(); static public Action s_fnAss = (ass) => { log.warn( $"Need to replace s_fnAss with custom function" ); }; public static void WatchPluginDir( string dir, Action fnAss ) { log.info( $"Watching {dir} for changes" ); s_fnAss = fnAss; s_watcher = new FileSystemWatcher( dir ); s_watcher.Created += OnCreated; s_watcher.Deleted += OnDeleted; s_watcher.Renamed += OnRenamed; s_watcher.Filter = "*.cs"; s_watcher.IncludeSubdirectories = true; s_watcher.EnableRaisingEvents = true; var existingFiles = Directory.GetFiles( dir, "*.cs", SearchOption.AllDirectories ); foreach( var filename in existingFiles ) { Process( filename ); } } static void OnCreated( object sender, FileSystemEventArgs fse ) { log.debug( $"{fse.Name} got {fse.ChangeType}" ); if( fse.Name?.EndsWith( ".cs" ) ?? false ) { Process( fse.FullPath ); } } static void OnDeleted( object sender, FileSystemEventArgs fse ) { log.debug( $"{fse.Name} got {fse.ChangeType}" ); } static void OnRenamed( object sender, FileSystemEventArgs fse ) { log.debug( $"{fse.Name} got {fse.ChangeType}" ); if( fse.Name?.EndsWith(".cs") ?? false ) { while( true ) { try { Process( fse.FullPath ); return; } catch( System.IO.IOException ex ) { log.debug( $"Ignoring {ex.Message}" ); } catch( Exception ex ) { log.error( $"Got ex {ex.GetType().Name} trying to process {fse.FullPath}" ); log.error( $"-> {ex.Message}" ); return; } Thread.Sleep( 100 ); } } } static void Process( string filename ) { CompileFile( filename, ( ass ) => { s_fnAss( ass ); }, ( diags ) => { } ); } public static void CompileFile( string filename, Action onSuccess, Action> onFailure, Platform platform = Platform.X86 ) { var fullpath = Path.GetFullPath( filename ); var stream = File.OpenRead( fullpath ); var sourceText = SourceText.From( stream ); Compile( sourceText, fullpath, onSuccess, onFailure ); } public static void Compile( string str, string uniquePath, Action onSuccess, Action> onFailure, Platform platform = Platform.X86 ) { var sourceText = SourceText.From( str ); Compile( sourceText, uniquePath, onSuccess, onFailure ); } public static void Compile( SourceText sourceText, string uniquePath, Action onSuccess, Action> onFailure, Platform platform = Platform.X86 ) { string assemblyName = Path.GetRandomFileName(); var options = new CSharpParseOptions(documentationMode: DocumentationMode.Diagnose, kind: SourceCodeKind.Regular); SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText( sourceText, options, uniquePath ); var memRef = new MemoryRefResolver(); using MemoryStream ms = new (); using MemoryStream pdb = new (); var result = CompileAndEmit(assemblyName, new[] { syntaxTree }, ms, pdb, platform); if (!result.Success) { if (onFailure == null) { } else { LogDiags( uniquePath, result.Diagnostics.Length, result.Diagnostics ); onFailure( result.Diagnostics ); } } else { ms.Seek(0, SeekOrigin.Begin); var assembly = Assembly.Load(ms.ToArray(), pdb.ToArray()); onSuccess( assembly ); } } public static void LogDiags( string uniquePath, int count, IEnumerable diags ) { log.warn( $"{count} Problems building script with name {uniquePath}" ); foreach( var diag in diags ) { log.debug( $"{diag}" ); } } private static EmitResult CompileAndEmit(string assemblyName, SyntaxTree[] syntaxTrees, MemoryStream ms, MemoryStream pdb, Platform platform) { MemoryRefResolver memRef = new(); // @@@@ TODO :: Config release / debug CSharpCompilation compilation = CSharpCompilation.Create( assemblyName, syntaxTrees: syntaxTrees, references: RefCache.References, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, sourceReferenceResolver: memRef, optimizationLevel: OptimizationLevel.Release, platform: platform, specificDiagnosticOptions: new Dictionary { { "CS1701", ReportDiagnostic.Suppress } })); return compilation.Emit(ms, pdb); } private static class RefCache { // create the list of references on first use, but if two threads both *start* making the list thats fine since we'll just use whichever wins. private static readonly Lazy> lazyRef = new Lazy>(GetReferences, LazyThreadSafetyMode.PublicationOnly); public static IReadOnlyList References => lazyRef.Value; private static ImmutableArray GetReferences() { var builder = ImmutableArray.CreateBuilder(); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach( var ass in assemblies ) { if( ass != null && !ass.IsDynamic && ass.Location != null ) { try { builder.Add( MetadataReference.CreateFromFile( ass.Location ) ); } catch( Exception ex ) { log.warn( $"Got {ex.GetType().Name} sayaing {ex.Message}" ); } } } return builder.ToImmutable(); } } public static string PrettyName( Type t ) { if( t.GetGenericArguments().Length == 0 ) { return t.FullName?.Replace( '+', '.' ) ?? ""; } var genArgs = t.GetGenericArguments(); var typeDef = t.FullName ?? ""; var indexOfTick = typeDef.IndexOf("`"); var unmangledOuterName = typeDef.Substring(0, typeDef.IndexOf('`')).Replace('+', '.'); var innerName = ""; //Check for inner class if( typeDef.ElementAt( indexOfTick + 2 ) != '[' ) { var indexOfOpenBracket = typeDef.IndexOf('[', indexOfTick); innerName = typeDef.Substring( indexOfTick + 2, indexOfOpenBracket - (indexOfTick + 2) ).Replace( '+', '.' ); } return unmangledOuterName + "<" + String.Join( ",", genArgs.Select( PrettyName ) ) + ">" + innerName; } private static void AddIfFirst(IDictionary dict, TKey key, TValue value) { if (!dict.ContainsKey(key)) { dict.Add(key, value); } } }