This repository has been archived on 2025-01-01. You can view files and clone it, but cannot push or open issues or pull requests.
ShiftOS_TheReturn/ShiftOS_TheReturn/TerminalBackend.cs
2017-07-29 11:01:32 -04:00

615 lines
22 KiB
C#

/*
* MIT License
*
* Copyright (c) 2017 Michael VanOverbeek and ShiftOS devs
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using static ShiftOS.Engine.SaveSystem;
namespace ShiftOS.Engine
{
/// <summary>
/// Backend for the ShiftOS terminal.
/// </summary>
public static class TerminalBackend
{
private static string _shellOverrideString = "";
/// <summary>
/// Gets the current shell prompt override string.
/// </summary>
public static string ShellOverride
{
get
{
return (string.IsNullOrWhiteSpace(_shellOverrideString) || SaveSystem.CurrentSave == null) ? $"{SaveSystem.CurrentSave.Username}@{SaveSystem.CurrentSave.SystemName}:~$ " : _shellOverrideString;
}
}
/// <summary>
/// Sets the shell override string to the specified value. Empty string or <see cref="null"/> to use the default ShiftOS string.
/// </summary>
/// <param name="value">The string to use as a shell prompt.</param>
public static void SetShellOverride(string value)
{
_shellOverrideString = value;
}
/// <summary>
/// Occurs when a command is processed.
/// </summary>
public static event Action<string, string> CommandProcessed;
/// <summary>
/// Gets or sets whether the current command is elevated.
/// </summary>
public static bool Elevated { get; set; }
/// <summary>
/// Parses command-line arguments using the ShiftOS syntax and stores them in a <see cref="Dictionary{string, string}"/>, removing the parsed text from the original string.
/// </summary>
/// <param name="text">The text to parse.</param>
/// <returns><see cref="Dictionary{string, string}"/> containing the parsed arguments.</returns>
public static Dictionary<string, string> GetArgs(ref string text)
{
bool shouldParse = false;
int argStart = 0;
if (text.Contains("{"))
{
shouldParse = true;
argStart = text.IndexOf('{');
}
if (shouldParse == false)
{
string replacement = Regex.Replace(text, @"\t|\n|\r", "");
text = replacement + "{}";
shouldParse = true;
argStart = text.IndexOf('{');
}
string args = text.Substring(argStart, text.Length - argStart);
text = text.Remove(argStart, text.Length - argStart).Replace(" ", "");
return JsonConvert.DeserializeObject<Dictionary<string, string>>(args);
}
/// <summary>
/// String representing the last command entered by the user.
/// </summary>
public static string LastCommand = "";
/// <summary>
/// Gets the output of the last command.
/// </summary>
public static string LastCommandBuffer { get; private set; }
/// <summary>
/// Invokes a ShiftOS terminal command.
/// </summary>
/// <param name="command">The command name.</param>
/// <param name="arguments">The command arguments.</param>
/// <param name="isRemote">Whether the command should be sent through Remote Terminal Session (RTS).</param>
public static void InvokeCommand(string command, Dictionary<string, string> arguments, bool isRemote = false)
{
try
{
bool commandWasClient = RunClient(command, arguments, isRemote);
if (!commandWasClient)
{
Console.WriteLine("{ERR_COMMANDNOTFOUND}");
}
CommandProcessed?.Invoke(command, JsonConvert.SerializeObject(arguments));
}
catch (Exception ex)
{
Console.WriteLine("{ERR_SYNTAXERROR}");
PrefixEnabled = true;
}
}
/// <summary>
/// Transforms a <see cref="Dictionary{String, String}"/> of arguments to a <see cref="Dictionary{String, Object}"/>.
/// </summary>
/// <param name="argss">The original argument dictionary to convert.</param>
/// <returns>The converted dictionary.</returns>
public static string GetSentArgs(Dictionary<string, string> argss)
{
Dictionary<string, object> args = new Dictionary<string, object>();
foreach (KeyValuePair<string, string> arg in argss)
{
args[arg.Key] = arg.Value;
}
return JsonConvert.SerializeObject(args);
}
public class TerminalCommand
{
public virtual bool MatchShell()
{
if(ShellMatch != "metacmd")
{
return (ShellMatch == _shellOverrideString);
}
return true;
}
public string ShellMatch { get; set; }
public override int GetHashCode()
{
int hash = 0;
foreach (char c in ToString())
{
hash += (int)c;
}
return hash;
}
public Command CommandInfo { get; set; }
public List<string> RequiredArguments { get; set; }
public string Dependencies { get; set; }
public MethodInfo CommandHandler;
public Type CommandType;
public override string ToString()
{
StringBuilder sb = new StringBuilder();
sb.Append(this.CommandInfo.name);
if (this.RequiredArguments.Count > 0)
{
sb.Append(" ");
foreach (var arg in RequiredArguments)
{
sb.Append("--" + arg);
sb.Append(" ");
if (RequiredArguments.IndexOf(arg) < RequiredArguments.Count - 1)
sb.Append(',');
}
sb.Append("}");
}
sb.Append("|");
sb.Append(CommandHandler.Name + "()");
return sb.ToString();
}
public bool RequiresElevation { get; set; }
public virtual void Invoke(Dictionary<string, object> args)
{
List<string> errors = new List<string>();
if (ShellMatch != "metacmd")
{
if (ShellMatch != TerminalBackend._shellOverrideString)
{
errors.Add("Command not found.");
}
}
if (errors.Count > 0)
{
foreach (var error in errors)
{
Console.WriteLine(error);
}
return;
}
try
{
CommandHandler.Invoke(null, new[] { args });
}
catch
{
CommandHandler.Invoke(null, null);
}
}
}
[MetaCommand]
[Command("exit")]
public static void Exit()
{
if (_shellOverrideString != "")
_shellOverrideString = "";
else
{
Console.WriteLine("error: cannot exit system shell");
}
}
public class WinOpenCommand : TerminalCommand
{
public Type ShiftOSWindow { get; set; }
public override bool MatchShell()
{
return (_shellOverrideString == "");
}
public override void Invoke(Dictionary<string, object> args)
{
AppearanceManager.SetupWindow((IShiftOSWindow)Activator.CreateInstance(ShiftOSWindow, null));
}
}
public class MemoryTextWriter : System.IO.TextWriter
{
public override Encoding Encoding
{
get
{
return Encoding.Unicode;
}
}
private StringBuilder sb = null;
public MemoryTextWriter()
{
sb = new StringBuilder();
}
public override string ToString()
{
return sb.ToString();
}
public override void Write(char value)
{
sb.Append(value);
}
public override void WriteLine()
{
sb.AppendLine();
}
public override void Write(string value)
{
sb.Append(value);
}
public override void Close()
{
sb.Clear();
sb = null;
base.Close();
}
public override void WriteLine(string value)
{
sb.AppendLine(value);
}
}
public static List<TerminalCommand> Commands { get; private set; }
public static void PopulateTerminalCommands()
{
Commands = new List<TerminalCommand>();
foreach (var type in ReflectMan.Types)
{
if (type.GetInterfaces().Contains(typeof(IShiftOSWindow)))
{
var winopenattrib = type.GetCustomAttributes(false).FirstOrDefault(x => x is WinOpenAttribute) as WinOpenAttribute;
if(winopenattrib != null)
{
var winc = new WinOpenCommand();
winc.CommandType = type;
var rupg = type.GetCustomAttributes().FirstOrDefault(x => x is RequiresUpgradeAttribute) as RequiresUpgradeAttribute;
if (rupg != null)
winc.Dependencies = rupg.Upgrade;
winc.CommandInfo = new Engine.Command(winopenattrib.ID, "", "Opens the \"" + winopenattrib.ID + " program.");
winc.RequiredArguments = new List<string>();
winc.RequiresElevation = false;
winc.ShiftOSWindow = type;
var ambiguity = Commands.FirstOrDefault(x => x.CommandInfo.name == winc.CommandInfo.name);
if (ambiguity != null)
throw new Exception("Ambiguity error. The program " + winc.CommandInfo.name + " collides with another program or terminal command with the same name. Please either change the already-existing program/command, or change this one's WinOpenAttribute value to compensate.");
Commands.Add(winc);
}
}
foreach (var mth in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
var cmd = mth.GetCustomAttributes(false).FirstOrDefault(x => x is Command);
if (cmd != null)
{
var tc = new TerminalCommand();
tc.RequiresElevation = !(type.GetCustomAttributes(false).FirstOrDefault(x => x is KernelModeAttribute) == null);
var shellConstraint = mth.GetCustomAttributes(false).FirstOrDefault(x => x is ShellConstraintAttribute) as ShellConstraintAttribute;
tc.ShellMatch = (shellConstraint == null) ? "" : shellConstraint.Shell;
if(mth.GetCustomAttributes(false).FirstOrDefault(x=>x is MetaCommandAttribute) != null)
{
tc.ShellMatch = "metacmd";
}
tc.CommandInfo = cmd as Command;
tc.RequiresElevation = tc.RequiresElevation || !(mth.GetCustomAttributes(false).FirstOrDefault(x => x is KernelModeAttribute) == null);
tc.RequiredArguments = new List<string>();
foreach (var arg in mth.GetCustomAttributes(false).Where(x => x is RequiresArgument))
{
var rarg = arg as RequiresArgument;
tc.RequiredArguments.Add(rarg.argument);
}
var rupg = mth.GetCustomAttributes(false).FirstOrDefault(x => x is RequiresUpgradeAttribute) as RequiresUpgradeAttribute;
if (rupg != null)
tc.Dependencies = rupg.Upgrade;
else
tc.Dependencies = "";
tc.CommandType = type;
tc.CommandHandler = mth;
var ambiguity = Commands.FirstOrDefault(x => x.CommandInfo.name == tc.CommandInfo.name);
if (ambiguity != null)
throw new Exception("Command ambiguity error. You can't have two commands with the same name: " + $"{tc} == {ambiguity}");
if (!Commands.Contains(tc))
Commands.Add(tc);
}
}
}
Console.WriteLine("[termdb] " + Commands.Count + " commands found.");
}
/// <summary>
/// Invokes a ShiftOS terminal command.
/// </summary>
/// <param name="text">The full command text in regular ShiftOS syntax</param>
/// <param name="isRemote">Whether the command should be sent through Remote Terminal Session (RTS).</param>
public static void InvokeCommand(string text, bool isRemote = false)
{
if (string.IsNullOrWhiteSpace(text))
return;
var tw = new MemoryTextWriter();
Console.SetOut(tw);
try
{
var args = GetArgs(ref text);
bool commandWasClient = RunClient(text, args, isRemote);
if (!commandWasClient)
{
Console.WriteLine("Error: Command not found.");
}
CommandProcessed?.Invoke(text, GetSentArgs(args));
}
catch (Exception ex)
{
Console.WriteLine($"Command parse error: {ex.Message}");
PrefixEnabled = true;
}
string buffer = tw.ToString();
LastCommandBuffer = buffer;
Console.SetOut(new TerminalTextWriter());
if(!isRemote)
Console.Write(buffer);
}
/// <summary>
/// Gets or sets whether the user prefix is printed after a command completes.
/// </summary>
public static bool PrefixEnabled { get; set; }
/// <summary>
/// Gets or sets whether the user is in a story plot, and thus, the terminal input should be disabled.
/// </summary>
public static bool InStory { get; set; }
/// <summary>
/// Another latest command string.
/// </summary>
public static string latestCommmand = "";
/// <summary>
/// Occurs when the engine requests a Terminal to be open.
/// </summary>
public static event EmptyEventHandler TerminalRequested;
/// <summary>
/// Opens a Terminal.
/// </summary>
internal static void OpenTerminal()
{
TerminalRequested?.Invoke();
}
/// <summary>
/// Determines if the specified command method can be ran in RTS
/// </summary>
/// <param name="mth">The method to scan</param>
/// <param name="isRemote">Is the user in an RTS session?</param>
/// <returns>Whether the command can be run.</returns>
public static bool CanRunRemotely(MethodInfo mth, bool isRemote)
{
if (!isRemote)
return true;
foreach (var attr in mth.GetCustomAttributes(false))
{
if (attr is RemoteLockAttribute)
return false;
}
return true;
}
/// <summary>
/// Runs a command on the client.
/// </summary>
/// <param name="text">The command text.</param>
/// <param name="argss">The command arguments.</param>
/// <param name="isRemote">Whether the command should be ran through RTS.</param>
/// <returns>Whether the command ran successfully.</returns>
public static bool RunClient(string text, Dictionary<string, string> argss, bool isRemote = false)
{
Dictionary<string, object> args = new Dictionary<string, object>();
foreach (KeyValuePair<string, string> arg in argss)
{
args[arg.Key] = arg.Value;
}
return RunClient(text, args, isRemote);
}
/// <summary>
/// Runs a command on the client.
/// </summary>
/// <param name="text">The command text.</param>
/// <param name="args">The command arguments.</param>
/// <param name="isRemote">Whether the command should be run in RTS.</param>
/// <returns>Whether the command ran successfully.</returns>
public static bool RunClient(string text, Dictionary<string, object> args, bool isRemote = false)
{
latestCommmand = text;
//Console.WriteLine(text + " " + "{" + string.Join(",", args.Select(kv => kv.Key + "=" + kv.Value).ToArray()) + "}" + " " + isRemote);
var cmd = Commands.FirstOrDefault(x => Localization.Parse(x.CommandInfo.name) == text);
if (cmd == null)
return false;
if (!Shiftorium.UpgradeInstalled(cmd.Dependencies))
return false;
bool res = false;
foreach (var arg in cmd.RequiredArguments)
{
if (!args.ContainsKey(arg))
{
res = true;
Console.WriteLine("You are missing an argument with the key \"" + arg + "\".");
}
}
if (res == true)
return true;
try
{
cmd.Invoke(args);
}
catch(Exception ex)
{
Console.WriteLine("Command error: " + ex.Message);
}
return true;
}
#if DEBUG
[Command("setshell", hide = true)]
[RequiresArgument("id")]
public static void Debug_SetShellOverrideCMD(Dictionary<string, object> args)
{
SetShellOverride(args["id"].ToString());
}
#endif
/// <summary>
/// Prints the user prompt to the terminal.
/// </summary>
public static void PrintPrompt()
{
if (PrefixEnabled)
{
Console.Write(ShellOverride);
ConsoleEx.Flush();
}
}
/// <summary>
/// Gets whether the terminal backend is forwarding console write requests through RTS to a remote client.
/// </summary>
public static bool IsForwardingConsoleWrites { get; internal set; }
/// <summary>
/// Gets the RTS forward GUID.
/// </summary>
public static string ForwardGUID { get; internal set; }
/// <summary>
/// Occurs when the user inputs text in a Terminal.
/// </summary>
public static event TextSentEventHandler TextSent;
/// <summary>
/// Fakes the user inputting text to a Terminal.
/// </summary>
/// <param name="text">The text to input.</param>
public static void SendText(string text)
{
TextSent?.Invoke(text);
}
}
/// <summary>
/// Marks this command so that it can be run in ANY shell.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class MetaCommandAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ShellConstraintAttribute : Attribute
{
/// <summary>
/// Instructs the terminal command interpreter to disallow running of this command unless the user shell override matches up with the value provided here.
/// </summary>
/// <param name="shell">The required shell string. Null or whitespace to match with the default ShiftOS shell.</param>
public ShellConstraintAttribute(string shell)
{
Shell = shell;
}
/// <summary>
/// Gets the required shell string for the command.
/// </summary>
public string Shell { get; private set; }
}
}