Added the ability to cancel stuff. #42

Merged
That-One-Nerd merged 3 commits from shell-systems into main 2023-04-01 14:24:45 -04:00
6 changed files with 175 additions and 22 deletions

View File

@ -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;

View File

@ -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
{
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,
}
}

View File

@ -1,6 +1,4 @@
using SrcMod.Shell.Interop;
namespace SrcMod.Shell.Modules;
namespace SrcMod.Shell.Modules;
[Module("clipboard")]
public static class ClipboardModule

View File

@ -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;
}
}

View File

@ -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<ParameterInfo>();
@ -34,10 +36,13 @@ public class CommandInfo
CommandAttribute[] attributes = info.GetCustomAttributes<CommandAttribute>().ToArray();
if (attributes.Length <= 0) return Array.Empty<CommandInfo>();
CanCancelAttribute? cancel = info.GetCustomAttribute<CanCancelAttribute>();
foreach (CommandAttribute attribute in attributes)
{
commands.Add(new()
{
CanBeCancelled = cancel is null || cancel.CanCancel,
Method = info,
Module = parentModule,
Name = info.Name,

View File

@ -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;
@ -16,6 +18,11 @@ public class Shell
public List<HistoryItem> History;
public string WorkingDirectory;
private bool lastCancel;
private bool printedCancel;
private BackgroundWorker? activeCommand;
public Shell()
{
Console.CursorVisible = false;
@ -79,6 +86,10 @@ public class Shell
Write(" by ", ConsoleColor.White, false);
Write($"{Author}", ConsoleColor.DarkYellow);
lastCancel = false;
activeCommand = null;
Console.CancelKeyPress += HandleCancel;
ActiveGame = null;
ReloadDirectoryInfo();
@ -106,8 +117,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 +125,52 @@ 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);
PlayWarningSound();
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<string> parts = new();
string active = string.Empty;
@ -171,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 = command.CanBeCancelled;
while (activeCommand is not null && activeCommand.IsBusy) Thread.Yield();
if (activeCommand is not null)
{
activeCommand.Dispose();
activeCommand = null;
}
return;
}
}
@ -196,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);
@ -205,4 +277,44 @@ public class Shell
if (ActiveMod is not null) title += $" - {ActiveMod.Name}";
Console.Title = title;
}
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)
{
// 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);
}
}