Skip to content

Instantly share code, notes, and snippets.

@SeeminglyScience
Created June 16, 2017 02:22
Show Gist options
  • Save SeeminglyScience/23f6f5d78f962ec8cae67d42b16fb86c to your computer and use it in GitHub Desktop.
Save SeeminglyScience/23f6f5d78f962ec8cae67d42b16fb86c to your computer and use it in GitHub Desktop.
PoC draft of using node-clr to work with PowerShell. Requires node modules "clr" and "events".
import { EventEmitter } from 'events'
var clr = require('clr');
var namespaces = clr.init({ assemblies: [ 'System.Management.Automation' ], global: false });
export function forEachClr (collection: any, callback: (item: any) => any) {
let enumerator = collection.GetEnumerator();
while (enumerator.MoveNext()) callback(enumerator.Current)
}
export var toJSObject = (clrObject: any, properties?: string[]) => {
if (!clr.isCLRObject(clrObject)) return clrObject
let newObject: any = {}
Object.keys(clrObject.__proto__).forEach(key => {
if (!(typeof properties === 'undefined' || properties.some(p => p === key))) return;
if (typeof clrObject[key] === 'function') return;
newObject[key] = clrObject[key];
// TODO: Find a good way to limit depth to avoid infinite recursion.
// newObject[key] = clr.isCLRObject(clrObject[key])
// ? toJSObject(clrObject[key])
// : clrObject[key];
})
return newObject;
}
export class PowerShell extends EventEmitter {
script: string;
instance: any;
private clr: any;
private namespaces: any;
constructor(script: string) {
super();
this.script = script;
this.clr = clr;
this.namespaces = namespaces;
this.on('error', (err: any) => console.error(err));
this.instance = this.namespaces
.System.Management.Automation.PowerShell
.Create()
.AddScript(script);
}
async invoke() : Promise<any[]> {
await this.addEmitters();
return new Promise<any[]>(
(resolve, reject) => {
let result: any[] = [];
forEachClr(this.instance.Invoke(), o => result.push(o));
if (this.instance.HadErrors) {
reject(this.instance.Streams.Error.get(0).Exception.Message);
} else {
resolve(result);
}
}
);
}
async beginInvoke() : Promise<void> {
// clr doesn't support generic methods like BeginInvoke, so we go through PowerShell to invoke.
// Because of this, we also have to create the output collection, and attach the on data added
// event in PowerShell.
await this.addEmitters();
await new Promise<void> (
(resolve, reject) => {
let runner = this.namespaces
.System.Management.Automation.PowerShell
.Create()
.AddScript(`
param ($powerShell, $callback)
end {
if ($callback) {
$collection = New-Object System.Management.Automation.PSDataCollection[psobject]
$newCallback = {
# Make sure the output isn't wrapped in a PSObject because they sometimes
# don't rehydrate properly on the JS side.
for ($output = $this[$PSItem.Index];
$output -is [psobject];
$output = $output.psobject.BaseObject) {}
$params = [object[]]::new(1)
$params[0] = $output
$callback.Invoke($params)
}
$collection.add_DataAdded($newCallback)
$collection = $collection.psobject.BaseObject
}
[powershell].GetMember(\'BeginInvoke\').
Where{ $PSItem.GetParameters().Count -eq 2 }.
MakeGenericMethod([psobject], [psobject]).
Invoke($powerShell, @($collection, $collection))
}`)
.AddParameter('powerShell', this.instance)
.AddParameter('callback', (outputStream: any) => {
this.emit(
'outputDataAdded',
this.hydrate(outputStream))});
runner.Invoke();
if (runner.HadErrors) {
reject(new Error(runner.Streams.Error.get(0).ToString()));
} else {
resolve()
}
}
)
return this.addInvocationEmitter()
}
private hydrate(clrObject: any) : any {
// HACK: Objects that come back from PS aren't hydrated correctly, to get around this
// we create a PSObject and unwrap it so it passes through clr's private "imbue"
// method again.
return this.clr.isCLRObject(clrObject)
? new this.namespaces.System.Management.Automation.PSObject(clrObject).BaseObject
: clrObject
}
private addEmitters() : Promise<void> {
// Sometimes Electron will crash if you try to set the handlers before PS is fully ready,
// so we only set them up before invoking.
return this.addRunspaceEmitter()
.then(() => this.addDataEmitter('Error'))
// These don't work currently.
// .then(() => this.addDataEmitter('Verbose'))
// .then(() => this.addDataEmitter('Warning'))
// .then(() => this.addDataEmitter('Debug'))
}
private addDataEmitter(streamName: string) : Promise<void> {
return new Promise<void>(
(resolve, reject) => {
try {
let eventName = streamName.toLowerCase();
this.instance.Streams[streamName].DataAdded.add(
(stream?: any, eventArgs?: any) => {
this.emit(
`${eventName}DataAdded`,
stream.get(eventArgs.Index))});
resolve();
} catch(e) {
reject(e)
}
}
)
}
private addInvocationEmitter() : Promise<void> {
return new Promise<void>(
(resolve, reject) => {
try {
this.instance.InvocationStateChanged.add(
(powerShell?: any, eventArgs?: any) => {
this.emit(
'invocationStateChanged',
eventArgs)});
resolve()
}
catch(e) {
reject(new Error('Unable to set Invocation emitter. Error: ' + e))
}
}
)
}
private addRunspaceEmitter() : Promise<void> {
return new Promise<void>(
(resolve, reject) => {
try {
this.instance.Runspace.AvailabilityChanged.add(
(runspace?: any, eventArgs?: any) => {
let state;
if (eventArgs === null || typeof eventArgs === 'undefined') {
state = 'Unknown'
} else {
state = eventArgs.RunspaceAvailability.ToString();
}
if (state === 'Available') this.emit('end');
if (state === 'Busy') this.emit('start');
this.emit(
'runspaceAvailabilityChanged',
state)});
resolve()
}
catch(e) {
reject(new Error('Unable to set Runspace emitter. Error: ' + e))
}
}
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment