From 5412b023a0374d58093a3b57b288c911a576e956 Mon Sep 17 00:00:00 2001 From: That_One_Nerd Date: Fri, 31 Mar 2023 18:12:20 -0400 Subject: [PATCH 1/3] Added some terminal interrupt systems. Later it will kill active commands. --- SrcMod/Shell/GlobalUsings.cs | 1 + SrcMod/Shell/Interop/Winmm.cs | 25 ++++++++++ SrcMod/Shell/Modules/ClipboardModule.cs | 4 +- SrcMod/Shell/Shell.cs | 66 +++++++++++++++++++++++-- 4 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 SrcMod/Shell/Interop/Winmm.cs diff --git a/SrcMod/Shell/GlobalUsings.cs b/SrcMod/Shell/GlobalUsings.cs index 1d90052..a26ef43 100644 --- a/SrcMod/Shell/GlobalUsings.cs +++ b/SrcMod/Shell/GlobalUsings.cs @@ -3,6 +3,7 @@ global using SharpCompress.Archives.Rar; global using SharpCompress.Archives.SevenZip; global using SharpCompress.Readers; global using SrcMod.Shell; +global using SrcMod.Shell.Interop; global using SrcMod.Shell.Modules.ObjectModels; global using System; global using System.Collections.Generic; diff --git a/SrcMod/Shell/Interop/Winmm.cs b/SrcMod/Shell/Interop/Winmm.cs new file mode 100644 index 0000000..58a6d67 --- /dev/null +++ b/SrcMod/Shell/Interop/Winmm.cs @@ -0,0 +1,25 @@ +namespace SrcMod.Shell.Interop; + +internal static partial class Winmm +{ + [LibraryImport("winmm.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool PlaySound([MarshalAs(UnmanagedType.LPStr)] string pszSound, nint hMod, uint fdwSound); + + public enum PlaySoundFlags : uint + { + SND_SYNC = 0x00000000, + SND_ASYNC = 0x00000001, + SND_NODEFAULT = 0x00000002, + SND_MEMORY = 0x00000004, + SND_LOOP = 0x00000008, + SND_NOSTOP = 0x00000010, + SND_PURGE = 0x00000040, + SND_APPLICATION = 0x00000080, + SND_NOWAIT = 0x00002000, + SND_ALIAS = 0x00010000, + SND_FILENAME = 0x00020000, + SND_RESOURCE = 0x00040000, + SND_ALIAS_ID = 0x00100000, + } +} diff --git a/SrcMod/Shell/Modules/ClipboardModule.cs b/SrcMod/Shell/Modules/ClipboardModule.cs index d69b42b..14a01c2 100644 --- a/SrcMod/Shell/Modules/ClipboardModule.cs +++ b/SrcMod/Shell/Modules/ClipboardModule.cs @@ -1,6 +1,4 @@ -using SrcMod.Shell.Interop; - -namespace SrcMod.Shell.Modules; +namespace SrcMod.Shell.Modules; [Module("clipboard")] public static class ClipboardModule diff --git a/SrcMod/Shell/Shell.cs b/SrcMod/Shell/Shell.cs index e62b406..02c25e1 100644 --- a/SrcMod/Shell/Shell.cs +++ b/SrcMod/Shell/Shell.cs @@ -16,6 +16,9 @@ public class Shell public List History; public string WorkingDirectory; + private bool lastCancel; + private bool printedCancel; + public Shell() { Console.CursorVisible = false; @@ -79,6 +82,9 @@ public class Shell Write(" by ", ConsoleColor.White, false); Write($"{Author}", ConsoleColor.DarkYellow); + lastCancel = false; + Console.CancelKeyPress += HandleCancel; + ActiveGame = null; ReloadDirectoryInfo(); @@ -106,8 +112,6 @@ public class Shell public string ReadLine() { - Console.CursorVisible = true; - Write($"\n{WorkingDirectory}", ConsoleColor.DarkGreen, false); if (ActiveGame is not null) Write($" {ActiveGame}", ConsoleColor.DarkYellow, false); if (ActiveMod is not null) Write($" {ActiveMod}", ConsoleColor.Magenta, false); @@ -116,17 +120,55 @@ public class Shell Write($" {Name}", ConsoleColor.DarkCyan, false); Write(" > ", ConsoleColor.White, false); + bool printed = false; + + if (lastCancel && !printedCancel) + { + // Print the warning. A little bit of mess because execution must + // continue without funny printing errors but it's alright I guess. + + int originalLeft = Console.CursorLeft; + + Console.CursorTop -= 3; + Write("Press ^C again to exit the shell.", ConsoleColor.Red); + // Send a warning sound. + + Winmm.PlaySound("SystemAsterisk", nint.Zero, + (uint)(Winmm.PlaySoundFlags.SND_ALIAS | Winmm.PlaySoundFlags.SND_ASYNC)); + + printedCancel = true; + Console.CursorTop += 2; + + Console.CursorLeft = originalLeft; + printed = true; + } + Console.ForegroundColor = ConsoleColor.White; + Console.CursorVisible = true; string message = Console.ReadLine()!; + Console.CursorVisible = false; Console.ResetColor(); - Console.CursorVisible = false; + if (!printed) + { + lastCancel = false; + printedCancel = false; + } return message; } public void InvokeCommand(string cmd) { + if (cmd is null) + { + // This usually won't happen, but might if for example + // the shell cancel interrupt is called. This probably + // happens for other shell interrupts are called. + Write(null); + return; + } + List parts = new(); string active = string.Empty; @@ -205,4 +247,22 @@ public class Shell if (ActiveMod is not null) title += $" - {ActiveMod.Name}"; Console.Title = title; } + + private void HandleCancel(object? sender, ConsoleCancelEventArgs args) + { + // Due to some funny multithreading issues, we want to make the warning label + // single-threaded on the shell. + if (!lastCancel) + { + // Enable the warning. The "ReadLine" method will do the rest. + lastCancel = true; + args.Cancel = true; // "Cancel" referring to the cancellation of the cancel operation. + return; + } + + // Actually kill the shell. We do still have to worry about some multithreaded + // nonsense, but a bearable amount of it. + Console.ResetColor(); + Environment.Exit(0); + } } From 1e5f0dba4ea3481cb4d146cec4530704aa2a9d75 Mon Sep 17 00:00:00 2001 From: That_One_Nerd Date: Sat, 1 Apr 2023 08:27:30 -0400 Subject: [PATCH 2/3] Added method interrupt handling. --- SrcMod/Shell/Interop/Winmm.cs | 26 +++++----- SrcMod/Shell/Shell.cs | 92 +++++++++++++++++++++++++++-------- 2 files changed, 85 insertions(+), 33 deletions(-) diff --git a/SrcMod/Shell/Interop/Winmm.cs b/SrcMod/Shell/Interop/Winmm.cs index 58a6d67..ac1e97a 100644 --- a/SrcMod/Shell/Interop/Winmm.cs +++ b/SrcMod/Shell/Interop/Winmm.cs @@ -8,18 +8,18 @@ internal static partial class Winmm public enum PlaySoundFlags : uint { - SND_SYNC = 0x00000000, - SND_ASYNC = 0x00000001, - SND_NODEFAULT = 0x00000002, - SND_MEMORY = 0x00000004, - SND_LOOP = 0x00000008, - SND_NOSTOP = 0x00000010, - SND_PURGE = 0x00000040, - SND_APPLICATION = 0x00000080, - SND_NOWAIT = 0x00002000, - SND_ALIAS = 0x00010000, - SND_FILENAME = 0x00020000, - SND_RESOURCE = 0x00040000, - SND_ALIAS_ID = 0x00100000, + Sync = 0x00000000, + Async = 0x00000001, + NoDefault = 0x00000002, + Memory = 0x00000004, + Loop = 0x00000008, + NoStop = 0x00000010, + Purge = 0x00000040, + Application = 0x00000080, + NoWait = 0x00002000, + Alias = 0x00010000, + FileName = 0x00020000, + Resource = 0x00040000, + AliasId = 0x00100000, } } diff --git a/SrcMod/Shell/Shell.cs b/SrcMod/Shell/Shell.cs index 02c25e1..4cb5190 100644 --- a/SrcMod/Shell/Shell.cs +++ b/SrcMod/Shell/Shell.cs @@ -1,10 +1,12 @@ -namespace SrcMod.Shell; +using System.ComponentModel; + +namespace SrcMod.Shell; public class Shell { public const string Author = "That_One_Nerd"; public const string Name = "SrcMod"; - public const string Version = "Alpha 0.3.0"; + public const string Version = "Alpha 0.3.1"; public readonly string? ShellDirectory; @@ -19,6 +21,8 @@ public class Shell private bool lastCancel; private bool printedCancel; + private BackgroundWorker? activeCommand; + public Shell() { Console.CursorVisible = false; @@ -83,6 +87,7 @@ public class Shell Write($"{Author}", ConsoleColor.DarkYellow); lastCancel = false; + activeCommand = null; Console.CancelKeyPress += HandleCancel; ActiveGame = null; @@ -131,10 +136,7 @@ public class Shell Console.CursorTop -= 3; Write("Press ^C again to exit the shell.", ConsoleColor.Red); - // Send a warning sound. - - Winmm.PlaySound("SystemAsterisk", nint.Zero, - (uint)(Winmm.PlaySoundFlags.SND_ALIAS | Winmm.PlaySoundFlags.SND_ASYNC)); + PlayWarningSound(); printedCancel = true; Console.CursorTop += 2; @@ -213,24 +215,41 @@ public class Shell int start = module.NameIsPrefix ? 2 : 1; string[] args = parts.GetRange(start, parts.Count - start).ToArray(); + void runCommand(object? sender, DoWorkEventArgs e) + { #if RELEASE - try - { + try + { #endif - command.Invoke(args); + command.Invoke(args); #if RELEASE - } - catch (TargetInvocationException ex) - { - Write($"[ERROR] {ex.InnerException!.Message}", ConsoleColor.Red); - if (LoadingBarEnabled) LoadingBarEnd(); - } - catch (Exception ex) - { - Write($"[ERROR] {ex.Message}", ConsoleColor.Red); - if (LoadingBarEnabled) LoadingBarEnd(); - } + } + catch (TargetInvocationException ex) + { + Write($"[ERROR] {ex.InnerException!.Message}", ConsoleColor.Red); + if (LoadingBarEnabled) LoadingBarEnd(); + } + catch (Exception ex) + { + Write($"[ERROR] {ex.Message}", ConsoleColor.Red); + if (LoadingBarEnabled) LoadingBarEnd(); + } #endif + } + + activeCommand = new(); + activeCommand.DoWork += runCommand; + activeCommand.RunWorkerAsync(); + + activeCommand.WorkerSupportsCancellation = true; + + while (activeCommand is not null && activeCommand.IsBusy) Thread.Yield(); + + if (activeCommand is not null) + { + activeCommand.Dispose(); + activeCommand = null; + } return; } } @@ -238,6 +257,17 @@ public class Shell Write($"[ERROR] Could not find command \"{cmd}\".", ConsoleColor.Red); } + private static void PlayErrorSound() + { + Winmm.PlaySound("SystemHand", nint.Zero, + (uint)(Winmm.PlaySoundFlags.Alias | Winmm.PlaySoundFlags.Async)); + } + private static void PlayWarningSound() + { + Winmm.PlaySound("SystemAsterisk", nint.Zero, + (uint)(Winmm.PlaySoundFlags.Alias | Winmm.PlaySoundFlags.Async)); + } + public void ReloadDirectoryInfo() { ActiveMod = Mod.ReadDirectory(WorkingDirectory); @@ -250,6 +280,28 @@ public class Shell private void HandleCancel(object? sender, ConsoleCancelEventArgs args) { + if (activeCommand is not null && activeCommand.IsBusy) + { + if (activeCommand.WorkerSupportsCancellation) + { + // Kill the active command. + activeCommand.CancelAsync(); + activeCommand.Dispose(); + activeCommand = null; + } + else + { + // Command doesn't support cancellation. + // Warn the user. + PlayErrorSound(); + } + + lastCancel = false; + printedCancel = false; + args.Cancel = true; + return; + } + // Due to some funny multithreading issues, we want to make the warning label // single-threaded on the shell. if (!lastCancel) From a7ff0a7ab40d94176447f50c2ef6c81e1377fe96 Mon Sep 17 00:00:00 2001 From: That_One_Nerd Date: Sat, 1 Apr 2023 08:50:08 -0400 Subject: [PATCH 3/3] Added the ability to prevent cancellation. --- .../Shell/Modules/ObjectModels/CanCancelAttribute.cs | 12 ++++++++++++ SrcMod/Shell/Modules/ObjectModels/CommandInfo.cs | 5 +++++ SrcMod/Shell/Shell.cs | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 SrcMod/Shell/Modules/ObjectModels/CanCancelAttribute.cs diff --git a/SrcMod/Shell/Modules/ObjectModels/CanCancelAttribute.cs b/SrcMod/Shell/Modules/ObjectModels/CanCancelAttribute.cs new file mode 100644 index 0000000..30f2451 --- /dev/null +++ b/SrcMod/Shell/Modules/ObjectModels/CanCancelAttribute.cs @@ -0,0 +1,12 @@ +namespace SrcMod.Shell.Modules.ObjectModels; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public class CanCancelAttribute : Attribute +{ + public readonly bool CanCancel; + + public CanCancelAttribute(bool canCancel) + { + CanCancel = canCancel; + } +} diff --git a/SrcMod/Shell/Modules/ObjectModels/CommandInfo.cs b/SrcMod/Shell/Modules/ObjectModels/CommandInfo.cs index 8505f35..0e71e6f 100644 --- a/SrcMod/Shell/Modules/ObjectModels/CommandInfo.cs +++ b/SrcMod/Shell/Modules/ObjectModels/CommandInfo.cs @@ -2,6 +2,7 @@ public class CommandInfo { + public bool CanBeCancelled { get; private set; } public required ModuleInfo Module { get; init; } public required MethodInfo Method { get; init; } public string Name { get; private set; } @@ -11,6 +12,7 @@ public class CommandInfo private CommandInfo() { + CanBeCancelled = false; Name = string.Empty; NameId = string.Empty; Parameters = Array.Empty(); @@ -34,10 +36,13 @@ public class CommandInfo CommandAttribute[] attributes = info.GetCustomAttributes().ToArray(); if (attributes.Length <= 0) return Array.Empty(); + CanCancelAttribute? cancel = info.GetCustomAttribute(); + foreach (CommandAttribute attribute in attributes) { commands.Add(new() { + CanBeCancelled = cancel is null || cancel.CanCancel, Method = info, Module = parentModule, Name = info.Name, diff --git a/SrcMod/Shell/Shell.cs b/SrcMod/Shell/Shell.cs index 4cb5190..a2b7cda 100644 --- a/SrcMod/Shell/Shell.cs +++ b/SrcMod/Shell/Shell.cs @@ -241,7 +241,7 @@ public class Shell activeCommand.DoWork += runCommand; activeCommand.RunWorkerAsync(); - activeCommand.WorkerSupportsCancellation = true; + activeCommand.WorkerSupportsCancellation = command.CanBeCancelled; while (activeCommand is not null && activeCommand.IsBusy) Thread.Yield();