Created
June 16, 2017 02:22
-
-
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".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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