diff --git a/src/Application.cs b/src/Application.cs index e32563a..6afb2ce 100644 --- a/src/Application.cs +++ b/src/Application.cs @@ -1,51 +1,60 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Globalization; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ObjectStream; using ObjectStream.Data; using Oxide.CompilerServices.Logging; using Oxide.CompilerServices.Settings; +using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Oxide.CompilerServices { internal sealed class Application { - public IServiceProvider Services { get; } - public ILogger logger { get; } - private OxideSettings settings { get; } - private ObjectStreamClient? objectStream { get; } - private CancellationTokenSource tokenSource { get; } - private Queue compilerQueue { get; } + private IServiceProvider Services { get; } + private ILogger Logger { get; } + private OxideSettings Settings { get; } + private ObjectStreamClient? ObjectStream { get; } + private CancellationTokenSource TokenSource { get; } + private Queue CompilerQueue { get; } + + private Task WorkerTask { get; set; } public Application(IServiceProvider provider, ILogger logger, OxideSettings options, CancellationTokenSource tk) { - tokenSource = tk; - this.logger = logger; - settings = options; + Program.ApplicationLogLevel.MinimumLevel = options.Logging.Level.ToSerilog(); + TokenSource = tk; + this.Logger = logger; + Settings = options; Services = provider; - compilerQueue = new Queue(); - ConfigureLogging(options); + CompilerQueue = new Queue(); - if (options.Compiler.EnableMessageStream) - { - objectStream = new ObjectStreamClient(Console.OpenStandardInput(), Console.OpenStandardOutput()); - objectStream.Message += (s, m) => OnMessage(m); - } + if (!options.Compiler.EnableMessageStream) return; + + ObjectStream = new ObjectStreamClient(Console.OpenStandardInput(), Console.OpenStandardOutput()); + ObjectStream.Message += (s, m) => OnMessage(m); } public void Start() { + Logger.LogInformation(Events.Startup, $"Starting compiler v{Assembly.GetExecutingAssembly().GetName().Version}. . ."); + Logger.LogInformation(Events.Startup, $"Minimal logging level is set to {Program.ApplicationLogLevel.MinimumLevel}"); + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + Thread.CurrentThread.Priority = ThreadPriority.BelowNormal; + Thread.CurrentThread.IsBackground = true; AppDomain.CurrentDomain.ProcessExit += (sender, e) => Exit("SIGTERM"); Console.CancelKeyPress += (s, o) => Exit("SIGINT (Ctrl + C)"); - if (settings.ParentProcess != null) + if (Settings.ParentProcess != null) { try { - if (!settings.ParentProcess.HasExited) + if (!Settings.ParentProcess.HasExited) { - settings.ParentProcess.EnableRaisingEvents = true; - settings.ParentProcess.Exited += (s, o) => Exit("parent process shutdown"); - logger.LogInformation(Events.Startup, "Watching parent process ([{id}] {name}) for shutdown", settings.ParentProcess.Id, settings.ParentProcess.ProcessName); + Settings.ParentProcess.EnableRaisingEvents = true; + Settings.ParentProcess.Exited += (s, o) => Exit("parent process shutdown"); + Logger.LogInformation(Events.Startup, "Watching parent process ([{id}] {name}) for shutdown", Settings.ParentProcess.Id, Settings.ParentProcess.ProcessName); } else { @@ -55,111 +64,48 @@ public void Start() } catch (Exception ex) { - logger.LogWarning(Events.Startup, ex, "Failed to attach to parent process, compiler may stay open if parent is improperly shutdown"); - } - } - - if (!settings.Compiler.EnableMessageStream) - { - logger.LogInformation(Events.Startup, "Compiler startup complete"); - LoopConsoleInput(); - } - else - { - objectStream!.Start(); - logger.LogDebug(Events.Startup, "Hooked into standard input/output for interprocess communication"); - logger.LogInformation(Events.Startup, "Compiler startup complete"); - Task.Delay(TimeSpan.FromSeconds(2), tokenSource.Token).Wait(); - objectStream.PushMessage(new CompilerMessage() { Type = CompilerMessageType.Ready }); - var task = new Task(Worker, TaskCreationOptions.LongRunning); - task.Start(); - task.Wait(); - } - } - - private void LoopConsoleInput() - { - if (tokenSource.IsCancellationRequested) - { - logger.LogInformation(Events.Shutdown, "Shutdown requested, killing console input loop."); - return; - } - string command = string.Empty; - Console.ForegroundColor = ConsoleColor.White; - Console.Write("Please type a command: "); - Console.ResetColor(); - - while (true) - { - if (tokenSource.IsCancellationRequested) - { - break; + Logger.LogWarning(Events.Startup, ex, "Failed to attach to parent process, compiler may stay open if parent is improperly shutdown"); } - ConsoleKeyInfo key = Console.ReadKey(true); - - switch (key.Key) - { - case ConsoleKey.Escape: - Console.Write(new string('\b', Console.CursorLeft)); - tokenSource.Cancel(); - break; - - case ConsoleKey.Enter: - if (!string.IsNullOrWhiteSpace(command)) - { - Console.Write(new string('\b', Console.CursorLeft)); - string[] args = new string[0]; - string value = OnCommand(command, args); - logger.LogInformation(Events.Command, $"Command: {command} | Result: {value}"); - Thread.Sleep(50); - } - else - { - continue; - } - break; - - case ConsoleKey.Backspace: - command = command.Substring(0, command.Length - 1); - Console.Write('\b'); - continue; - - default: - command += key.KeyChar; - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Write(key.KeyChar); - Console.ResetColor(); - continue; - } - break; } - LoopConsoleInput(); - } - private string OnCommand(string command, string[] args) - { - return "Unhandled"; + if (!Settings.Compiler.EnableMessageStream) return; + + Logger.LogDebug(Events.Startup, "Started message server. . ."); + ObjectStream!.Start(); + Logger.LogInformation(Events.Startup, "Message server has started"); + Task.Delay(TimeSpan.FromSeconds(2), TokenSource.Token).Wait(); + ObjectStream.PushMessage(new CompilerMessage() { Type = CompilerMessageType.Ready }); + Logger.LogInformation(Events.Startup, "Sent ready message to parent process"); + + Task task = Task.Factory.StartNew( + function: Worker, + cancellationToken: TokenSource.Token, + creationOptions: TaskCreationOptions.LongRunning, + scheduler: TaskScheduler.Default + ); + + WorkerTask = task.Unwrap(); + Logger.LogDebug(Events.Startup, "Compiler has started successfully and is awaiting jobs. . ."); + WorkerTask.Wait(); } private void OnMessage(CompilerMessage message) { - if (tokenSource.IsCancellationRequested) + if (TokenSource.IsCancellationRequested) { - logger.LogDebug(Events.Command, "OnMessage: Cancel has been requested"); return; } switch (message.Type) { case CompilerMessageType.Compile: - lock (compilerQueue) + lock (CompilerQueue) { - message.Client = objectStream; + message.Client = ObjectStream; CompilerData data = (CompilerData)message.Data; data.Message = message; - data.SourceFiles = data.SourceFiles.OrderBy(f => f.Name).ToArray(); - data.ReferenceFiles = data.ReferenceFiles.OrderBy(f => f.Name).ToArray(); - compilerQueue.Enqueue(message); + Logger.LogDebug(Events.Compile, $"Received compile job {message.Id} | Plugins: {data.SourceFiles.Length}, References: {data.ReferenceFiles.Length}"); + CompilerQueue.Enqueue(message); } break; @@ -169,30 +115,37 @@ private void OnMessage(CompilerMessage message) } } - private async void Worker() + private async Task Worker() { - CancellationToken token = tokenSource.Token; - while (!token.IsCancellationRequested) + CompilerMessage message = null; + while (!TokenSource.IsCancellationRequested) { - CompilerMessage message; - lock (compilerQueue) + + lock (CompilerQueue) { - if (compilerQueue.Count == 0) + if (CompilerQueue.Count != 0) { - continue; + message = CompilerQueue.Dequeue(); } + else + { + message = null; + } + } - message = compilerQueue.Dequeue(); + if (message == null) + { + await Task.Delay(1000); + continue; } CompilerData data = (CompilerData)message.Data; ICompilerService compiler = Services.GetRequiredService(); - logger.LogDebug(Events.Compile, "Starting compile job {id}", message.Id); await compiler.Compile(message.Id, data); } } - public void Exit(string? source) + private void Exit(string? source) { string message = "Termination request has been received"; if (!string.IsNullOrWhiteSpace(message)) @@ -200,32 +153,8 @@ public void Exit(string? source) message += $" from {source}"; } - logger.LogInformation(Events.Shutdown, message); - tokenSource.Cancel(); - } - - private void ConfigureLogging(OxideSettings settings) - { - NLog.Config.LoggingConfiguration config = NLog.LogManager.Configuration; - NLog.Targets.FileTarget file = new(); - config.AddTarget("file", file); - file.FileName = Path.Combine(settings.Path.Logging, settings.Logging.FileName); - file.Layout = "(${time})[${level}] ${logger:shortName=true}[${event-properties:item=EventId}]: ${message}${onexception:inner= ${newline}${exception:format=ToString,Data}}"; - file.AutoFlush = true; - file.CreateDirs = true; - file.Encoding = settings.DefaultEncoding; - NLog.LogLevel level = settings.Logging.Level switch - { - LogLevel.Debug => NLog.LogLevel.Debug, - LogLevel.Warning => NLog.LogLevel.Warn, - LogLevel.Critical or LogLevel.Error => NLog.LogLevel.Error, - LogLevel.Trace => NLog.LogLevel.Trace, - _ => NLog.LogLevel.Info, - }; - NLog.Config.LoggingRule rule = new("*", level, file); - config.LoggingRules.Add(rule); - NLog.LogManager.Configuration = config; - logger.LogDebug(Events.Startup, "Configured file logging for '{0}' and higher to {1}", settings.Logging.Level, file.FileName.ToString()); + Logger.LogInformation(Events.Shutdown, message); + TokenSource.Cancel(); } } } diff --git a/src/CSharp/CSharpLanguage.cs b/src/CSharp/CSharpLanguage.cs index a8d8818..3120a18 100644 --- a/src/CSharp/CSharpLanguage.cs +++ b/src/CSharp/CSharpLanguage.cs @@ -7,9 +7,11 @@ using Oxide.CompilerServices.Logging; using Oxide.CompilerServices.Settings; using System.Collections.Immutable; +using System.Diagnostics; using System.Globalization; using System.Text; using System.Text.RegularExpressions; +using Serilog.Events; namespace Oxide.CompilerServices.CSharp { @@ -32,25 +34,75 @@ public CSharpLanguage(ILogger logger, OxideSettings settings, IS { _logger = logger; _settings = settings; - logger.LogDebug(Events.Startup, "C# for {version} Initialized!", AppContext.TargetFrameworkName); _token = token.Token; _services = provider; } public async Task Compile(int id, CompilerData data) { - _logger.LogInformation(Events.Compile, "====== Compilation Job {id} ======", id); + Stopwatch stopwatch = Stopwatch.StartNew(); + _logger.LogInformation(Events.Compile, $"Starting compilation of job {id} | Total Plugins: {data.SourceFiles.Length}"); + string details = + $"Settings[Encoding: {data.Encoding}, CSVersion: {data.CSharpVersion()}, Target: {data.OutputKind()}, Platform: {data.Platform()}, StdLib: {data.StdLib}, Debug: {data.Debug}]"; + + if (Program.ApplicationLogLevel.MinimumLevel < LogEventLevel.Debug) + { + if (data.ReferenceFiles.Length > 0) + { + details += Environment.NewLine + $"Reference Files:" + Environment.NewLine; + for (int i = 0; i < data.ReferenceFiles.Length; i++) + { + CompilerFile reference = data.ReferenceFiles[i]; + if (i > 0) + { + details += Environment.NewLine; + } + + details += $" - [{i + 1}] {Path.GetFileName(reference.Name)}({reference.Data.Length})"; + } + } + + if (data.SourceFiles.Length > 0) + { + details += Environment.NewLine + $"Plugin Files:" + Environment.NewLine; + + for (int i = 0; i < data.SourceFiles.Length; i++) + { + CompilerFile plugin = data.SourceFiles[i]; + if (i > 0) + { + details += Environment.NewLine; + } + + details += $" - [{i + 1}] {Path.GetFileName(plugin.Name)}({plugin.Data.Length})"; + } + } + } + + + + _logger.LogDebug(Events.Compile, details); + try { CompilerMessage message = await SafeCompile(data, new CompilerMessage() { Id = id, Type = CompilerMessageType.Assembly, Client = data.Message.Client }); - if (((CompilationResult)message.Data).Data.Length > 0) _logger.LogInformation(Events.Compile, "==== Compilation Finished {id} | Success ====", id); - else _logger.LogInformation(Events.Compile, "==== Compilation Finished {id} | Failed ====", id); - message.Client!.PushMessage(message); + CompilationResult result = message.Data as CompilationResult; + + if (result.Data.Length > 0) + { + _logger.LogInformation(Events.Compile, $"Successfully compiled {result.Success}/{data.SourceFiles.Length} plugins for job {id} in {stopwatch.ElapsedMilliseconds}ms"); + } + else + { + _logger.LogError(Events.Compile, $"Failed to compile job {id} in {stopwatch.ElapsedMilliseconds}ms"); + } + message.Client!.PushMessage(message); + _logger.LogDebug(Events.Compile, $"Pushing job {id} back to parent"); } catch (Exception e) { - _logger.LogError(Events.Compile, e, "==== Compilation Error {id} ====", id); + _logger.LogError(Events.Compile, e, $"Error while compiling job {id} - {e.Message}"); data.Message.Client!.PushMessage(new CompilerMessage() { Id = id, Type = CompilerMessageType.Error, Data = e }); } } @@ -61,7 +113,6 @@ private async Task SafeCompile(CompilerData data, CompilerMessa if (data.SourceFiles == null || data.SourceFiles.Length == 0) throw new ArgumentException("No source files provided", nameof(data.SourceFiles)); OxideResolver resolver = (OxideResolver)_services.GetRequiredService(); - _logger.LogDebug(Events.Compile, GetJobStructure(data)); Dictionary references = new(StringComparer.OrdinalIgnoreCase); @@ -86,15 +137,6 @@ private async Task SafeCompile(CompilerData data, CompilerMessa case ".cs": case ".exe": case ".dll": - if (references.ContainsKey(fileName)) - { - _logger.LogDebug(Events.Compile, "Replacing existing project reference: {ref}", fileName); - } - else - { - _logger.LogDebug(Events.Compile, "Adding project reference: {ref}", fileName); - } - references[fileName] = File.Exists(reference.Name) && (reference.Data == null || reference.Data.Length == 0) ? MetadataReference.CreateFromFile(reference.Name) : MetadataReference.CreateFromImage(reference.Data, filePath: reference.Name); continue; @@ -103,12 +145,12 @@ private async Task SafeCompile(CompilerData data, CompilerMessa continue; } } + _logger.LogDebug(Events.Compile, $"Added {references.Count} project references"); } Dictionary trees = new(); Encoding encoding = Encoding.GetEncoding(data.Encoding); CSharpParseOptions options = new(data.CSharpVersion()); - _logger.LogDebug(Events.Compile, "Parsing source files using C# {version} with encoding {encoding}", data.CSharpVersion(), encoding.WebName); foreach (var source in data.SourceFiles) { string fileName = Path.GetFileName(source.Name); @@ -119,33 +161,38 @@ private async Task SafeCompile(CompilerData data, CompilerMessa return ((char)int.Parse(match.Value.Substring(2), NumberStyles.HexNumber)).ToString(); }, RegexOptions.Compiled | RegexOptions.IgnoreCase); + if (isUnicode) + { + _logger.LogDebug(Events.Compile, $"Plugin {fileName} is using unicode escape sequence"); + } + SyntaxTree tree = CSharpSyntaxTree.ParseText(sourceString, options, path: fileName, encoding: encoding, cancellationToken: _token); trees.Add(source, tree); - _logger.LogDebug(Events.Compile, "Added C# file {file} to the project", fileName); } - + _logger.LogDebug(Events.Compile, $"Added {trees.Count} plugins to the project"); CSharpCompilationOptions compOptions = new(data.OutputKind(), metadataReferenceResolver: resolver, platform: data.Platform(), allowUnsafe: true, optimizationLevel: data.Debug ? OptimizationLevel.Debug : OptimizationLevel.Release); CSharpCompilation comp = CSharpCompilation.Create(Path.GetRandomFileName(), trees.Values, references.Values, compOptions); - CompilationResult? payload = CompileProject(comp, message); + CompilationResult result = new() + { + Name = comp.AssemblyName + }; + + message.Data = result; + CompileProject(comp, message, result); return message; } - private CompilationResult CompileProject(CSharpCompilation compilation, CompilerMessage message) + private void CompileProject(CSharpCompilation compilation, CompilerMessage message, CompilationResult compResult) { using MemoryStream pe = new(); - //using MemoryStream pdb = new(); - EmitResult result = compilation.Emit(pe, cancellationToken: _token); + if (result.Success) { - CompilationResult data = new() - { - Name = compilation.AssemblyName, - Data = pe.ToArray() - }; - message.Data = data; - return data; + compResult.Data = pe.ToArray(); + compResult.Success = compilation.SyntaxTrees.Length; + return; } bool modified = false; @@ -172,6 +219,7 @@ private CompilationResult CompileProject(CSharpCompilation compilation, Compiler compilation = compilation.RemoveSyntaxTrees(tree); message.ExtraData += $"[Error][{diag.Id}][{fileName}] {diag.GetMessage()} | Line: {line}, Pos: {charPos} {Environment.NewLine}"; modified = true; + compResult.Failed++; } } else @@ -182,32 +230,8 @@ private CompilationResult CompileProject(CSharpCompilation compilation, Compiler if (modified && compilation.SyntaxTrees.Length > 0) { - return CompileProject(compilation, message); - } - - - CompilationResult r = new() - { - Name = compilation.AssemblyName! - }; - - message.Data = r; - return r; - } - - private static string GetJobStructure(CompilerData data) - { - StringBuilder builder = new(); - builder.AppendLine($"Encoding: {data.Encoding}, Target: {data.CSharpVersion()}, Output: {data.OutputKind()}, Optimize: {!data.Debug}"); - builder.AppendLine($"== Source Files ({data.SourceFiles.Length}) =="); - builder.AppendLine(string.Join(", ", data.SourceFiles.Select(s => $"[{s.Data.Length}] {s.Name}"))); - - if (data.ReferenceFiles != null && data.ReferenceFiles.Length > 0) - { - builder.AppendLine($"== Reference Files ({data.ReferenceFiles.Length}) =="); - builder.AppendLine(string.Join(", ", data.ReferenceFiles.Select(r => $"[{r.Data.Length}] {r.Name}"))); + CompileProject(compilation, message, compResult); } - return builder.ToString(); } } } diff --git a/src/ObjectStream/Data/CompilationResult.cs b/src/ObjectStream/Data/CompilationResult.cs index bb2d573..20cfcbc 100644 --- a/src/ObjectStream/Data/CompilationResult.cs +++ b/src/ObjectStream/Data/CompilationResult.cs @@ -7,6 +7,8 @@ public class CompilationResult public byte[] Data { get; set; } public byte[] Symbols { get; set; } + [NonSerialized] public int Success, Failed; + public CompilationResult() { Data = Array.Empty(); diff --git a/src/OxideResolver.cs b/src/OxideResolver.cs index 387dce1..0be8c57 100644 --- a/src/OxideResolver.cs +++ b/src/OxideResolver.cs @@ -60,7 +60,6 @@ public override ImmutableArray ResolveReference(str if (fileSystem.Exists) { - logger.LogDebug(Events.Compile, "Found from libraries directory: [Size: {Size}] {Name}", fileSystem.Length, fileSystem.Name); reference = MetadataReference.CreateFromFile(fileSystem.FullName); referenceCache.Add(reference); return reference; @@ -70,13 +69,12 @@ public override ImmutableArray ResolveReference(str if (fileSystem.Exists) { - logger.LogDebug(Events.Compile, "Found from runtime directory: [Size: {Size}] {Name}", fileSystem.Length, fileSystem.Name); reference = MetadataReference.CreateFromFile(fileSystem.FullName); referenceCache.Add(reference); return reference; } - logger.LogDebug(Events.Compile, "Missing assembly definition {name}", name); + logger.LogError(Events.Compile, "Unable to find required dependency {name}", name); return null; } }