feat: Add Get-Calendar command and Debug harnesses
- add Get-Calendar command - add initial debug harness for local debug
This commit is contained in:
parent
468f3c64ce
commit
274d279732
15 changed files with 938 additions and 1 deletions
216
PowershellHarness/CustomHost.cs
Normal file
216
PowershellHarness/CustomHost.cs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Management.Automation;
|
||||
using System.Management.Automation.Host;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
|
||||
namespace PowershellHarness;
|
||||
|
||||
// A whole bunch of empty implementations just so cmdlets have access to anything within Host.Ui (and probably any cmdlets
|
||||
// that prompt for information later)
|
||||
// https://github.com/leechristensen/OffensivePowerShellTasking/blob/master/OffensivePowerShellTasking/CustomPSHost.cs#L13
|
||||
public class CustomHost : PSHost
|
||||
{
|
||||
public override string Name => "Custom Host";
|
||||
public override Version Version { get; } = new Version(0, 0, 0, 0);
|
||||
public override Guid InstanceId { get; } = Guid.NewGuid();
|
||||
|
||||
private CustomUiHost _ui = new CustomUiHost();
|
||||
public override CustomUiHost UI => _ui;
|
||||
|
||||
public override CultureInfo CurrentCulture { get; } = CultureInfo.CurrentCulture;
|
||||
public override CultureInfo CurrentUICulture { get; } = CultureInfo.CurrentUICulture;
|
||||
|
||||
public CustomHost(int width, int height = 100)
|
||||
{
|
||||
_ui.RawUI.WindowSize = new() { Width = width, Height = height };
|
||||
}
|
||||
|
||||
public override void EnterNestedPrompt()
|
||||
{
|
||||
throw new NotImplementedException("EnterNestedPrompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
|
||||
public override void ExitNestedPrompt()
|
||||
{
|
||||
throw new NotImplementedException("ExitNestedPrompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
|
||||
public override void NotifyBeginApplication()
|
||||
{
|
||||
}
|
||||
|
||||
public override void NotifyEndApplication()
|
||||
{
|
||||
}
|
||||
|
||||
public override void SetShouldExit(int exitCode)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/leechristensen/OffensivePowerShellTasking/blob/d1b498d874948f41e6dc053204c511fa6fc11c9c/OffensivePowerShellTasking/CustomPSHostUserInterface.cs#L8
|
||||
public class CustomUiHost : PSHostUserInterface
|
||||
{
|
||||
// The only stuff that really matters to expose a host for any cmdlets
|
||||
private readonly CustomRawUiHost _rawUI = new CustomRawUiHost();
|
||||
public override CustomRawUiHost RawUI => _rawUI;
|
||||
|
||||
// Replace StringBuilder with whatever your preferred output method is (e.g. a socket or a named pipe)
|
||||
public StringBuilder output { get; set; }
|
||||
|
||||
public CustomUiHost()
|
||||
{
|
||||
output = new StringBuilder();
|
||||
}
|
||||
|
||||
public CustomUiHost(ref StringBuilder sb)
|
||||
{
|
||||
output = sb;
|
||||
}
|
||||
|
||||
public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
|
||||
{
|
||||
//output.Append("!").Append(value);
|
||||
}
|
||||
|
||||
public override void WriteLine()
|
||||
{
|
||||
//output.Append("!").Append("\n");
|
||||
}
|
||||
|
||||
public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
|
||||
{
|
||||
//output.Append("!").Append(value + "\n");
|
||||
}
|
||||
|
||||
public override void Write(string value)
|
||||
{
|
||||
//output.Append("!").Append(value);
|
||||
}
|
||||
|
||||
public override void WriteDebugLine(string message)
|
||||
{
|
||||
//output.Append("!").AppendLine("DEBUG: " + message);
|
||||
}
|
||||
|
||||
public override void WriteErrorLine(string value)
|
||||
{
|
||||
//output.Append("!").AppendLine("ERROR: " + value);
|
||||
}
|
||||
|
||||
public override void WriteLine(string value)
|
||||
{
|
||||
//output.Append("!").AppendLine(value);
|
||||
}
|
||||
|
||||
public override void WriteVerboseLine(string message)
|
||||
{
|
||||
//output.Append("!").AppendLine("VERBOSE: " + message);
|
||||
}
|
||||
|
||||
public override void WriteWarningLine(string message)
|
||||
{
|
||||
//output.Append("!").AppendLine("WARNING: " + message);
|
||||
}
|
||||
|
||||
public override void WriteProgress(long sourceId, ProgressRecord record)
|
||||
{
|
||||
}
|
||||
|
||||
public string Output => output.ToString();
|
||||
|
||||
public override Dictionary<string, PSObject> Prompt(string caption, string message, System.Collections.ObjectModel.Collection<FieldDescription> descriptions)
|
||||
{
|
||||
throw new NotImplementedException("Prompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
|
||||
public override int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection<ChoiceDescription> choices, int defaultChoice)
|
||||
{
|
||||
throw new NotImplementedException("PromptForChoice is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
|
||||
public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options)
|
||||
{
|
||||
throw new NotImplementedException("PromptForCredential1 is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
|
||||
public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName)
|
||||
{
|
||||
throw new NotImplementedException("PromptForCredential2 is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
|
||||
public override string ReadLine()
|
||||
{
|
||||
throw new NotImplementedException("ReadLine is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
|
||||
public override SecureString ReadLineAsSecureString()
|
||||
{
|
||||
throw new NotImplementedException("ReadLineAsSecureString is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/leechristensen/OffensivePowerShellTasking/blob/master/OffensivePowerShellTasking/CustomPSRHostRawUserInterface.cs#L8
|
||||
public class CustomRawUiHost : PSHostRawUserInterface
|
||||
{
|
||||
public override ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black;
|
||||
|
||||
public override Size BufferSize { get; set; } = new Size { Width = 100, Height = 1000 };
|
||||
|
||||
public override Coordinates CursorPosition { get; set; } = new Coordinates { X = 0, Y = 0 };
|
||||
|
||||
public override int CursorSize { get; set; } = 1;
|
||||
|
||||
public override void FlushInputBuffer()
|
||||
{
|
||||
throw new NotImplementedException("FlushInputBuffer is not implemented.");
|
||||
}
|
||||
|
||||
public override ConsoleColor ForegroundColor { get; set; } = ConsoleColor.White;
|
||||
|
||||
public override BufferCell[,] GetBufferContents(Rectangle rectangle)
|
||||
{
|
||||
throw new NotImplementedException("GetBufferContents is not implemented.");
|
||||
}
|
||||
|
||||
public override bool KeyAvailable
|
||||
{
|
||||
get { throw new NotImplementedException("KeyAvailable is not implemented."); }
|
||||
}
|
||||
|
||||
public override Size MaxPhysicalWindowSize { get; } = new Size
|
||||
{
|
||||
Width = int.MaxValue,
|
||||
Height = int.MaxValue
|
||||
};
|
||||
|
||||
public override Size MaxWindowSize { get; } = new Size { Width = 100, Height = 100 };
|
||||
|
||||
public override KeyInfo ReadKey(ReadKeyOptions options)
|
||||
{
|
||||
throw new NotImplementedException("ReadKey is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
|
||||
}
|
||||
|
||||
public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill)
|
||||
{
|
||||
throw new NotImplementedException("ScrollBufferContents is not implemented");
|
||||
}
|
||||
|
||||
public override void SetBufferContents(Rectangle rectangle, BufferCell fill)
|
||||
{
|
||||
throw new NotImplementedException("SetBufferContents is not implemented.");
|
||||
}
|
||||
|
||||
public override void SetBufferContents(Coordinates origin, BufferCell[,] contents)
|
||||
{
|
||||
throw new NotImplementedException("SetBufferContents is not implemented");
|
||||
}
|
||||
|
||||
public override Coordinates WindowPosition { get; set; } = new Coordinates { X = 0, Y = 0 };
|
||||
|
||||
public override Size WindowSize { get; set; } = new Size { Width = 120, Height = 100 };
|
||||
|
||||
public override string WindowTitle { get; set; } = "";
|
||||
}
|
||||
18
PowershellHarness/PowershellHarness.csproj
Normal file
18
PowershellHarness/PowershellHarness.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.6.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PowershellModule\PowershellModule.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
145
PowershellHarness/Program.cs
Normal file
145
PowershellHarness/Program.cs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
using System.Management.Automation;
|
||||
using System.Management.Automation.Runspaces;
|
||||
using System.Text;
|
||||
using PowershellModule.Calendar;
|
||||
|
||||
namespace PowershellHarness;
|
||||
|
||||
// https://github.com/FuseCP/FuseCP/blob/278a19dc06949600f25a1b4ed74d0419a8fa3fc2/FuseCP/Sources/FuseCP.Providers.HostedSolution.SfB2015/SfBBase.cs#L224
|
||||
// some useful code in here tbh
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// I've gotta work out how to get the rider terminal to act closer to powershell because .PadLeft in powershell
|
||||
// will correctly output lines when output with WriteOutput, but the Jetbrains debug worker (or whatever
|
||||
// it is that rider spawns), has none of that and operates in its own world.
|
||||
// Which is fine if you never want new lines.
|
||||
// Ideally you'd just launch pwsh.exe and attach to that, but _that_ has its own issues as well.
|
||||
// The new line issue might be related to how I've set up the host seeing as I just did a bunch of copy paste shit
|
||||
// from something I found on github to get it across the line so I could debug it.
|
||||
// I'll eventually revisit those hosts and rewrite them properly once I start adding more commands, and if I care.
|
||||
// Threads & variables has all the visuals I need after all.
|
||||
#if DEBUG
|
||||
if (!System.Diagnostics.Debugger.IsAttached)
|
||||
{
|
||||
Console.BackgroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(CenterText("Running without a debugger attached may result in lines not being output correctly"));
|
||||
Console.ResetColor();
|
||||
}
|
||||
#endif
|
||||
Console.WriteLine(CenterText(" PowerShell Debug Harness "));
|
||||
Console.WriteLine($"Reported console window size: {Console.WindowWidth}x{Console.WindowHeight}");
|
||||
Console.WriteLine($"Reported console buffer size: {Console.BufferWidth}x{Console.BufferHeight}");
|
||||
Console.WriteLine(CenterText(" WIDTH ", '-'));
|
||||
|
||||
var host = new CustomHost(Console.WindowWidth);
|
||||
var runspace = InitialisePowershellHost(host);
|
||||
|
||||
// InvokeCommand(runspace, GetCalendarCommand.FullName);
|
||||
|
||||
foreach (var day in Enum.GetValues<DayOfWeek>())
|
||||
{
|
||||
InvokeCommand(runspace, GetCalendarCommand.FullName, [
|
||||
CreateCommand(nameof(GetCalendarCommand.MarkedDaySymbol), "faker"),
|
||||
CreateCommand(nameof(GetCalendarCommand.StartOfWeek), day.ToString()),
|
||||
CreateCommand(nameof(GetCalendarCommand.Date), "22/6/26"),
|
||||
// CreateCommand(nameof(GetCalendarCommand.AlignRight))
|
||||
]);
|
||||
}
|
||||
|
||||
// InvokeCommand(runspace, GetCalendarCommand.FullName, [
|
||||
// new CommandParameter(nameof(GetCalendarCommand.MarkedDaySymbol), "🤫"),
|
||||
// new CommandParameter(nameof(GetCalendarCommand.StartOfWeek), DayOfWeek.Saturday)
|
||||
// ]);
|
||||
// InvokeCommand(runspace, GetCalendarCommand.FullName, [new CommandParameter(nameof(GetCalendarCommand.StartOfWeek), "Sunday")]);
|
||||
}
|
||||
|
||||
private static CommandParameter CreateCommand(string name, string? argument = null)
|
||||
{
|
||||
if (argument is not null)
|
||||
{
|
||||
return new CommandParameter(name, argument);
|
||||
}
|
||||
|
||||
return new CommandParameter(name);
|
||||
}
|
||||
|
||||
static string CenterText(string text, char paddingChar = ' ')
|
||||
{
|
||||
return text.PadLeft((Console.WindowWidth + text.Length) / 2, paddingChar).PadRight(Console.WindowWidth, paddingChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a PowerShell session and returns an open Runspace
|
||||
/// </summary>
|
||||
/// <param name="host"></param>
|
||||
/// <returns></returns>
|
||||
static Runspace InitialisePowershellHost(CustomHost host)
|
||||
{
|
||||
// Create the initial session state for the host. Yes, that 2 on the end does indicate that the underlying
|
||||
// implementation for this is C++
|
||||
// Welcome to the lands of fuck all documentation and figuring shit out from an increasingly shit internet
|
||||
// where finding non-slop answers gets harder by the day as people close sources of information to prevent
|
||||
// scraping. Fuck every single person involved modern AI.
|
||||
// It wasn't much better finding documentation about the System.Management.* namespace before that, but it's
|
||||
// definitely a lot worse because of it
|
||||
var initialSessionState = InitialSessionState.CreateDefault2();
|
||||
|
||||
|
||||
// No idea what the helpFileName should be. As is common with _a lot_ of the System.Management.* namespace,
|
||||
// fuck all is actually documented with comments, or documented at all!
|
||||
// From https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/creating-a-constrained-runspace?view=powershell-7.6
|
||||
// (which yes, this section _is_ under a legacy category!), it's perfectly fine to leave it as null.
|
||||
// We also don't really care as we're just doing this so we can test commands without having to deal with Rider
|
||||
// and it's quirks around runnning a powershell terminal and attaching to it. Yeah it technically works, but
|
||||
// it's way too common to end up with Rider refusing to build correctly or ensure the right dll is used for the module
|
||||
var getCalendarCommand = new SessionStateCmdletEntry(GetCalendarCommand.FullName, typeof(GetCalendarCommand), null);
|
||||
initialSessionState.Commands.Add(getCalendarCommand);
|
||||
|
||||
// Create a runspace from the state, open and return it
|
||||
var runspace = RunspaceFactory.CreateRunspace(host, initialSessionState);
|
||||
|
||||
// A runspace is technically disposable, but we're not reimplementing a full host and we won't be doing anything
|
||||
// that would require a fresh runspace multiple times over (yet), so we can just treat the lifetime of the
|
||||
// disposable as application lifetime
|
||||
runspace.Open();
|
||||
|
||||
return runspace;
|
||||
}
|
||||
|
||||
static void InvokeCommand(Runspace runspace, string command, IEnumerable<CommandParameter>? parameters = null)
|
||||
{
|
||||
// Not too sure on the difference of runspace vs PowerShell here. Doesn't seem to make a difference either way
|
||||
// and this is just a debug harness so it doesn't really matter for now
|
||||
using var pipeline = runspace.CreatePipeline();
|
||||
// using var powershell = PowerShell.Create(runspace);
|
||||
|
||||
var cmd = new Command(command);
|
||||
if (parameters is not null)
|
||||
{
|
||||
foreach (var commandParameter in parameters)
|
||||
{
|
||||
cmd.Parameters.Add(commandParameter);
|
||||
}
|
||||
}
|
||||
|
||||
pipeline.Commands.Add(cmd);
|
||||
// powershell.Commands.AddCommand(cmd);
|
||||
|
||||
try
|
||||
{
|
||||
var results = pipeline.Invoke();
|
||||
// var results = powershell.Invoke();
|
||||
foreach (var result in results)
|
||||
{
|
||||
Console.Write(result);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Unable to execute command {command}");
|
||||
Console.Error.WriteLine(ex.GetBaseException().Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue