diff --git a/Kernel.cs b/Kernel.cs index b499b20..4d17d62 100644 --- a/Kernel.cs +++ b/Kernel.cs @@ -1,7 +1,7 @@ global using Sys = Cosmos.Kernel.System; using Cosmos.Kernel.Core; - +using RemSox.Logging; using RemSox.Processes; using RemSox.Processing; using RemSox.Processing.IPC; @@ -30,7 +30,8 @@ public class Kernel : Sys.Kernel new ListProcessesCommand(), new StopProcessCommand(), new StartGuiCommand(), - new StopGuiCommand() + new StopGuiCommand(), + new ViewProcessLogs() ]); Sys.Mouse.MouseManager.Initialize(); @@ -62,19 +63,6 @@ public class Kernel : Sys.Kernel } } - - -[AttributeUsage(AttributeTargets.Class)] -public class TestAttribute : Attribute -{ - public string Name { get; set; } = "default"; -} - -[Test(Name = "HelloCosmos")] -public class TestClass -{ -} - public class TestProcess() : Process("Test Process") { internal override void Run(string[] args) diff --git a/Logging/ILogger.cs b/Logging/ILogger.cs new file mode 100644 index 0000000..11b34c4 --- /dev/null +++ b/Logging/ILogger.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RemSox.Logging +{ + public interface ILogger + { + void Log(string message, LogSeverity severity); + + void LogInfo(string message); + + void LogError(string message); + + void LogWarning(string message); + + IEnumerable GetLogs(int? count = null); + } +} \ No newline at end of file diff --git a/Logging/InMemoryLogger.cs b/Logging/InMemoryLogger.cs new file mode 100644 index 0000000..4469631 --- /dev/null +++ b/Logging/InMemoryLogger.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Concurrent; + +namespace RemSox.Logging +{ + public class InMemoryLogger : ILogger + { + readonly ConcurrentBag logs = []; + + public void Log(string message, LogSeverity severity) + { + logs.Add(new LogEntry(message, severity, DateTimeOffset.UtcNow)); + } + + public void LogError(string message) + { + Log(message, LogSeverity.Error); + } + + public void LogInfo(string message) + { + Log(message, LogSeverity.Info); + } + + public void LogWarning(string message) + { + Log(message, LogSeverity.Warning); + } + + public IEnumerable GetLogs(int? count = null) + { + IEnumerable orderedLogs = logs.OrderBy(l => l.Timestamp); + return count.HasValue ? orderedLogs.TakeLast(count.Value) : orderedLogs; + } + } +} \ No newline at end of file diff --git a/Logging/LogEntry.cs b/Logging/LogEntry.cs new file mode 100644 index 0000000..0231972 --- /dev/null +++ b/Logging/LogEntry.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RemSox.Logging +{ + public record LogEntry(string Message, LogSeverity Severity, DateTimeOffset Timestamp); +} \ No newline at end of file diff --git a/Logging/LogSeverity.cs b/Logging/LogSeverity.cs new file mode 100644 index 0000000..c0ee16d --- /dev/null +++ b/Logging/LogSeverity.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RemSox.Logging +{ + public enum LogSeverity + { + Info, + Warning, + Error + } +} \ No newline at end of file diff --git a/Logging/ProxyLogger.cs b/Logging/ProxyLogger.cs new file mode 100644 index 0000000..4750c26 --- /dev/null +++ b/Logging/ProxyLogger.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace RemSox.Logging +{ + public class ProxyLogger(IEnumerable loggers) : ILogger + { + public void Log(string message, LogSeverity severity) + { + foreach (var logger in loggers) + { + logger.Log(message, severity); + } + } + + public void LogError(string message) + { + Log(message, LogSeverity.Error); + } + + public void LogInfo(string message) + { + Log(message, LogSeverity.Info); + } + + public void LogWarning(string message) + { + Log(message, LogSeverity.Warning); + } + + public IEnumerable GetLogs(int? count = null) + { + return loggers.SelectMany(logger => logger.GetLogs(count / loggers.Count())).OrderBy(entry => entry.Timestamp); + } + } +} \ No newline at end of file diff --git a/Processing/Process.cs b/Processing/Process.cs index bf84c9a..b1a3301 100644 --- a/Processing/Process.cs +++ b/Processing/Process.cs @@ -1,10 +1,13 @@ +using RemSox.Logging; using RemSox.Processing.IPC; namespace RemSox.Processing; public abstract class Process(string name) { - public int Id { get; init; } + public int Id { get; init; } // Will be set by ProcessManager when the process is spawned + + public ILogger Logger { protected get; init; } = null!; // Will be set by ProcessManager when the process is spawned public string Name { get; set; } = name; diff --git a/Processing/ProcessManager.cs b/Processing/ProcessManager.cs index c9f65f7..74fa716 100644 --- a/Processing/ProcessManager.cs +++ b/Processing/ProcessManager.cs @@ -1,3 +1,4 @@ +using RemSox.Logging; using RemSox.UI.GUI.Windows; using System.Collections.Concurrent; @@ -10,22 +11,34 @@ public static class ProcessManager private static readonly ConcurrentDictionary> processesByType = new(); + private static readonly InMemoryLogger logger = new(); + private static readonly ConcurrentDictionary processLoggers = new(); + private static int nextProcessId = 0; private static int nextSystemProcessId = -1; public static int SpawnProcess(string[]? args = null) where T : Process, new() { + logger.Log($"Attempting to spawn process of type {typeof(T).Name}...", LogSeverity.Info); + if (ProcessManifest.HasFlag(ProcessManifest.ProcessManifestFlags.Singleton) && IsProcessRunning()) { + logger.Log($"Cannot spawn process of type {typeof(T).Name} because it is marked as a singleton and an instance is already running.", LogSeverity.Warning); throw new InvalidOperationException($"An instance of process type {typeof(T).Name} is already running."); } int id = ProcessManifest.HasFlag(ProcessManifest.ProcessManifestFlags.System) ? GetNextSystemProcessId() : GetNextProcessId(); + InMemoryLogger processLogger = new(); + ProxyLogger proxyLogger = new([logger, processLogger]); + + processLoggers.TryAdd(id, processLogger); + T process = new() { - Id = id + Id = id, + Logger = proxyLogger }; processesByType.AddOrUpdate(typeof(T), _ => [id], (_, set) => @@ -40,6 +53,10 @@ public static class ProcessManager { process.Run(args ?? []); } + catch (Exception ex) + { + logger.Log($"Process {process.Name} (ID: {process.Id}) terminated with an exception: {ex}", LogSeverity.Error); + } finally { processes.TryRemove(id, out _); @@ -52,12 +69,18 @@ public static class ProcessManager } WindowManager.CloseWindowsForProcess(id); + + logger.Log($"Process {process.Name} (ID: {process.Id}) has stopped.", LogSeverity.Info); + + processLoggers.TryRemove(id, out _); } }); processes.TryAdd(id, (process, thread)); thread.Start(); + logger.Log($"Spawned process {process.Name} of type {typeof(T).Name} with ID {id}.", LogSeverity.Info); + return id; } @@ -68,6 +91,7 @@ public static class ProcessManager return; } + logger.Log($"Requesting stop of process {entry.Process.Name} (ID: {entry.Process.Id}).", LogSeverity.Info); entry.Process.RequestStop(); } @@ -78,7 +102,10 @@ public static class ProcessManager return; } + logger.Log($"Requesting stop of process {entry.Process.Name} (ID: {entry.Process.Id}).", LogSeverity.Info); entry.Process.RequestStop(); + + logger.Log($"Waiting for process {entry.Process.Name} (ID: {entry.Process.Id}) to stop.", LogSeverity.Info); await Task.Run(() => entry.Thread.Join()); } @@ -141,6 +168,21 @@ public static class ProcessManager return false; } + public static IEnumerable GetLogs(int? count = null) + { + return logger.GetLogs(count); + } + + public static IEnumerable GetProcessLogs(int processId, int? count = null) + { + if (processLoggers.TryGetValue(processId, out InMemoryLogger? processLogger)) + { + return processLogger.GetLogs(count); + } + + return []; + } + private static int GetNextProcessId() { return nextProcessId++; diff --git a/UI/CLI/Commands/ViewProcessLogs.cs b/UI/CLI/Commands/ViewProcessLogs.cs new file mode 100644 index 0000000..74415b9 --- /dev/null +++ b/UI/CLI/Commands/ViewProcessLogs.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using RemSox.Logging; +using RemSox.Processing; + +namespace RemSox.UI.CLI.Commands +{ + public class ViewProcessLogs : ICommand + { + public string Name => "logs"; + + public string Description => "View logs for a process"; + + public void Execute(string? arguments, Action printLine) + { + if (int.TryParse(arguments, out int processId)) + { + IEnumerable logs = ProcessManager.GetProcessLogs(processId); + PrintLogs(logs, printLine); + } + else + { + IEnumerable logs = ProcessManager.GetLogs(); + PrintLogs(logs, printLine); + } + } + + private static void PrintLogs(IEnumerable logs, Action printLine) + { + if (!logs.Any()) + { + printLine("No logs found!"); + return; + } + + foreach (LogEntry log in logs) + { + printLine($"[{log.Timestamp:HH:mm:ss}] [{log.Severity}] {log.Message}"); + } + } + } +} \ No newline at end of file