Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created December 10, 2024 06:25
Show Gist options
  • Save jborean93/43c8338accc3f0eee95f8c43fec61e6f to your computer and use it in GitHub Desktop.
Save jborean93/43c8338accc3f0eee95f8c43fec61e6f to your computer and use it in GitHub Desktop.
Code to run a PowerShell named pipe server as a Task
using System;
using System.Management.Automation.Remoting;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
/*
This code uses internal APIs of the PowerShell remoting system to create the
named pipe listener and is not guaranteed to work in the future. It has been
tested against PowerShell 7.4.x and as PowerShell does not normally operate
multiple pipe server in the same process there could be unknown side effects
of using this. This code is provided as a proof of concept and should not be
used in production code.
This can be loaded and run in PowerShell through:
$pwshServerCode = Get-Content PwshPipeServer.cs -Raw
Add-Type -TypeDefinition $pwshServerCode
while ($true) {
$task = [PowerShellServer.NamedPipeListener]::RunServerAsync("foo")
while (-not $task.AsyncWaitHandle.WaitOne(300)) {}
if (-not $task.GetAwaiter().GetResult()) { break }
}
Otherwise it can be included in a C# project like any other Task.
*/
namespace PowerShellServer;
public static class NamedPipeListener
{
private static Type ListenerEndedEventArgs_Type;
private static Type NamedPipeProcessMediator_Type;
private static Type OutOfProcessMediatorBase_Type;
private static Type PSRemotingCryptoHelperServer_Type;
private static Type RemoteSessionNamedPipeServer_ListenerEnded_EventType;
private static ConstructorInfo NamedPipeProcessMediator_Ctor;
private static ConstructorInfo PSRemotingCryptoHelperServer_Ctor;
private static ConstructorInfo RemoteSessionNamedPipeServer_Ctor;
private static EventInfo RemoteSessionNamedPipeServer_ListenerEnded_Event;
private static MethodInfo RemoteSessionNamedPipeServer_ListenerEnded_EventAdd;
private static MethodInfo RemoteSessionNamedPipeServer_ListenerEnded_EventRemove;
private static MethodInfo OutOfProcessMediatorBase_Start;
private static MethodInfo RemoteSessionNamedPipeServer_StartListening;
private static PropertyInfo ListenerEndedEventArgs_Reason;
private static PropertyInfo ListenerEndedEventArgs_RestartListener;
static NamedPipeListener()
{
ListenerEndedEventArgs_Type = typeof(RemoteSessionNamedPipeServer).Assembly.GetType(
"System.Management.Automation.Remoting.ListenerEndedEventArgs")
?? throw new Exception("Failed to find ListenerEndedEventArgs type");
NamedPipeProcessMediator_Type = typeof(RemoteSessionNamedPipeServer).Assembly.GetType(
"System.Management.Automation.Remoting.Server.NamedPipeProcessMediator")
?? throw new Exception("Failed to find NamedPipeProcessMediator type");
OutOfProcessMediatorBase_Type = typeof(RemoteSessionNamedPipeServer).Assembly.GetType(
"System.Management.Automation.Remoting.Server.OutOfProcessMediatorBase")
?? throw new Exception("Failed to find OutOfProcessMediatorBase type");
PSRemotingCryptoHelperServer_Type = typeof(RemoteSessionNamedPipeServer).Assembly.GetType(
"System.Management.Automation.Internal.PSRemotingCryptoHelperServer")
?? throw new Exception("Failed to find PSRemotingCryptoHelperServer type");
NamedPipeProcessMediator_Ctor = NamedPipeProcessMediator_Type.GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { typeof(RemoteSessionNamedPipeServer) })
?? throw new Exception("Failed to find NamedPipeProcessMediator constructor");
PSRemotingCryptoHelperServer_Ctor = PSRemotingCryptoHelperServer_Type.GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { })
?? throw new Exception("Failed to find PSRemotingCryptoHelperServer constructor");
RemoteSessionNamedPipeServer_Ctor = typeof(RemoteSessionNamedPipeServer).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { typeof(string) })
?? throw new Exception("Failed to find RemoteSessionNamedPipeServer constructor");
RemoteSessionNamedPipeServer_ListenerEnded_Event = typeof(RemoteSessionNamedPipeServer).GetEvent(
"ListenerEnded",
BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new Exception("Failed to find RemoteSessionNamedPipeServer.ListenerEnded event");
RemoteSessionNamedPipeServer_ListenerEnded_EventAdd = RemoteSessionNamedPipeServer_ListenerEnded_Event.GetAddMethod(true)
?? throw new Exception("Failed to find RemoteSessionNamedPipeServer.ListenerEnded event add method");
RemoteSessionNamedPipeServer_ListenerEnded_EventRemove = RemoteSessionNamedPipeServer_ListenerEnded_Event.GetRemoveMethod(true)
?? throw new Exception("Failed to find RemoteSessionNamedPipeServer.ListenerEnded event remove method");
OutOfProcessMediatorBase_Start = OutOfProcessMediatorBase_Type.GetMethod(
"Start",
BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { typeof(string), PSRemotingCryptoHelperServer_Type, typeof(string), typeof(string), typeof(string) })
?? throw new Exception("Failed to find OutOfProcessMediatorBase.Start method");
RemoteSessionNamedPipeServer_StartListening = typeof(RemoteSessionNamedPipeServer).GetMethod(
"StartListening",
BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { typeof(Action<RemoteSessionNamedPipeServer>) })
?? throw new Exception("Failed to find RemoteSessionNamedPipeServer.StartListening method");
ListenerEndedEventArgs_Reason = ListenerEndedEventArgs_Type.GetProperty(
"Reason",
BindingFlags.Public | BindingFlags.Instance)
?? throw new Exception("Failed to find ListenerEndedEventArgs.Reason property");
ListenerEndedEventArgs_RestartListener = ListenerEndedEventArgs_Type.GetProperty(
"RestartListener",
BindingFlags.Public | BindingFlags.Instance)
?? throw new Exception("Failed to find ListenerEndedEventArgs.RestartListener property");
RemoteSessionNamedPipeServer_ListenerEnded_EventType = RemoteSessionNamedPipeServer_ListenerEnded_Event.EventHandlerType
?? throw new Exception("Failed to find RemoteSessionNamedPipeServer.ListenerEnded event type");
}
/// <summary>
/// Starts a named pipe listener under the pipe name specified.
/// This pipe can be connected to by a PowerShell client by specifying the
/// custom pipe name.
/// </summary>
/// <remarks>
/// On Windows, using Enter-PSHostProcess will result in two attempted
/// connections to the named pipe. The first connection is a test that
/// the pipe exists and is available, the second is the actual runspace
/// open. This task will have to be run straight after the first task is
/// completed for the client connection to succeed. Using non-Windows or
/// NamedPipeConnectionInfo class will not have this issue.
/// </remarks>
/// <param name="pipeName">The name of the pipe to listen with.</param>
/// <param name="configurationName">
/// The configuration endpoint name in which PowerShell is run. This can be
/// any endpoint registered on the local machine.
/// </param>
/// <param name="configurationFile">
/// SPecifies a session configuration (.pssc) file path. The configuration
/// contained in the configuration file will be applied to the PowerShell
/// session.
/// </param>
/// <param name="initialLocation">
/// The initial location to set the PowerShell session to.
/// </param>
/// <param name="initialCommand"></param>
/// The initial command/script to run when the client connects to the
/// session. This can be used to setup the session for the client.
/// <param name="cancellationToken"></param>
/// <returns>
/// A Task which returns a bool if the listener should be restarted or not.
/// Typically this is true unless there was an exception during the task.
/// </returns>
public static async Task<bool> RunServerAsync(
string pipeName,
string? configurationName = null,
string? configurationFile = null,
string? initialLocation = null,
string? initialCommand = null,
CancellationToken cancellationToken = default)
{
configurationName = string.IsNullOrWhiteSpace(configurationName) ? null : configurationName;
configurationFile = string.IsNullOrWhiteSpace(configurationFile) ? null : configurationFile;
initialLocation = string.IsNullOrWhiteSpace(initialLocation) ? null : initialLocation;
initialCommand = string.IsNullOrWhiteSpace(initialCommand) ? null : initialCommand;
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
using CancellationTokenRegistration cancelReg = cancellationToken.Register(() => tcs.TrySetCanceled());
Action<RemoteSessionNamedPipeServer> clientConnected = (server) =>
{
try
{
object namedProcMediator = NamedPipeProcessMediator_Ctor.Invoke(new object[] { server });
object cryptoHelper = PSRemotingCryptoHelperServer_Ctor.Invoke(new object[] { });
OutOfProcessMediatorBase_Start.Invoke(namedProcMediator, new object[] {
initialCommand!, cryptoHelper, initialLocation!, configurationName!, configurationFile!
});
}
catch (Exception e)
{
tcs.TrySetException(e);
}
};
Delegate listenerEnded = Delegate.CreateDelegate(
RemoteSessionNamedPipeServer_ListenerEnded_EventType,
new Action<object, EventArgs>((sender, args) =>
{
if (ListenerEndedEventArgs_Reason.GetValue(args) is Exception reason)
{
tcs.TrySetException(reason);
}
else
{
object? restartListener = ListenerEndedEventArgs_RestartListener.GetValue(args);
tcs.TrySetResult(restartListener is bool b && b);
}
}),
typeof(Action<object, EventArgs>).GetMethod("Invoke")!);
using RemoteSessionNamedPipeServer server = (RemoteSessionNamedPipeServer)RemoteSessionNamedPipeServer_Ctor.Invoke(
new object[] { pipeName });
RemoteSessionNamedPipeServer_ListenerEnded_EventAdd.Invoke(
server,
new object[] { listenerEnded });
try
{
RemoteSessionNamedPipeServer_StartListening.Invoke(
server,
new object[] { clientConnected });
return await tcs.Task;
}
finally
{
RemoteSessionNamedPipeServer_ListenerEnded_EventRemove.Invoke(
server,
new object[] { listenerEnded });
}
}
}
#nullable disable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment