PowerShell Tiny Project 11 - A marriage made in heaven; NodeJS & PowerShell

PowerShell Tiny Project 11 - A marriage made in heaven; NodeJS & PowerShell

Running PowerShell scripts from Node.js

As capable as Node.js is, there are scenarios in which Node.js doesn't have native support. For example, if you wanted to interact with Outlook via ActiveX/COM object, you'll have to rely on third-party packages.

You see PowerShell is deeply integrated with the Windows ecosystem, so there are several tasks related to Windows administration and management that are more straightforward in PowerShell compared to Node.js. Here are a few scenarios:

  1. Windows Management Instrumentation (WMI) Integration: While Node.js does not natively support WMI queries, PowerShell can easily interact with WMI using cmdlets like Get-WmiObject.

  2. Active Directory Management: PowerShell has direct cmdlets for managing Active Directory (Get-ADUser, New-ADUser, etc.) through the ActiveDirectory module, making it more natural for AD tasks compared to Node.js.

  3. Windows Event Logs: Accessing and managing Windows Event Logs is natively supported in PowerShell with cmdlets like Get-EventLog. While there are packages in Node.js to accomplish this, it’s not as seamless as in PowerShell.

  4. Registry Operations: PowerShell allows for direct interactions with the Windows registry using cmdlets like Get-ItemProperty, Set-ItemProperty, etc. In Node.js, you'd need third-party packages or external calls.

  5. Managing Windows Services: PowerShell provides a set of cmdlets (Get-Service, Stop-Service, etc.) for managing Windows services directly. In Node.js, this would require an external package or system calls.

  6. Managing Windows Features: Installing or removing Windows features is straightforward in PowerShell using Install-WindowsFeature or Uninstall-WindowsFeature. Doing so from Node.js would typically involve calling external commands or scripts.

  7. Windows Filesystem ACL Management: Setting permissions on files and folders with ACLs (Access Control Lists) is built into PowerShell (Get-Acl, Set-Acl). While you can manipulate file permissions in Node.js, getting into the granularity of ACLs would require more work.

  8. Direct Interaction with COM/ActiveX: As illustrated with the Outlook example, PowerShell has native COM support, making interactions with COM objects relatively straightforward. In Node.js, you'd need packages like winax.

As you can see above, we're not short of examples like this where getting something done is much easier in PowerShell by nothing but the virtue of how deep it's integrated within the operating system. This is not to mention that PowerShell's foundation on the .NET framework and with that comes certain advantages such as direct access to .NET libraries, windows GUI applications, and advance threading and parallelism.

With that said, many believe that PowerShell is best to be used as a scripting language and for API designing and backend development it is best to rely on Node.js and its rich ecosystem that is comprised of Express.js and many packages that are meant for backend development. Today's tiny project is focused on running your PowerShell scripts from your Node.js server environment. When these two technologies are combined, the possibilities become endless. The idea has many applications but two immediate ones are:

  1. For PowerShell developers who desire to have a graphical user interface, but found the learning curve of Windows Forms and Windows Presentation Foundation(WPF) unreasonable and rigid.
  1. For Node.JS and other backend developers that find it easier to use a language that comes with built-in and native operations.

let's get to our tiny project and see how we can run PowerShell from a Node.js environment. There are probably a few scenarios here. There easiest is for a script that doesnt require any parameters and one liners. Advanced functiosn that require pre-loading are probably more involved in terms of maintenance and execution. There are special cases that are more interesting such as the execution of scripts that have to run in a sequence.

const { exec } = require('child_process');

exec('powershell.exe Get-ChildItem C:\\', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

Here, we're using the built-in child_process module in node.js to spawn a new PowerShell process and execute a single command. If the PowerShell command that you run returns an object, we can capture that object by parsing the output of the command using a JSON parser.

const { exec } = require('child_process');

exec('powershell.exe Get-Process | ConvertTo-Json', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  const processList = JSON.parse(stdout);
  console.log(processList);
});

What about those who run node.js in on a mac environment?

const { exec } = require('child_process');

exec('pwsh -Command "Get-Process | ConvertTo-Json"', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  const processList = JSON.parse(stdout);
  console.log(processList);
});

not much changes, but I did run into the following execution error: RangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]: stdout maxBuffer length exceeded

The RangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]: stdout maxBuffer length exceeded error occurs when the output of the command that you are executing with child_process.exec() exceeds the maximum size allowed for the stdout buffer. By default, the stdout buffer size is set to 200KB, but you can increase this limit by passing a maxBuffer option to the exec() function.

To fix this error, you can increase the size of the maxBuffer option to a larger value. For example, to set the maxBuffer size to 1MB (1048576 bytes), you can modify the example code as follows:

const { exec } = require('child_process');

const maxBuffer = 1048576; // 1MB in bytes

exec('powershell.exe Get-ChildItem C:\\', { maxBuffer }, (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

In this example, we are passing the maxBuffer option with a value of 1048576 bytes (1MB) to the exec() function. This increases the size of the stdout buffer and should prevent the RangeError from occurring.

Another common problem is JSON truncation and max depth so might see this error JSON is truncated as serialization has exceeded the set depth of 2

This error message occurs when the JSON output of a command exceeds the maximum serialization depth allowed by the ConvertTo-Json PowerShell command. This error message is followed by the SyntaxError: Unexpected token n JSON at position 0 error message, which indicates that the JSON output cannot be parsed by the JSON.parse() method because it is truncated.

To fix this error, you can increase the maximum serialization depth allowed by the ConvertTo-Json command using the -Depth parameter. The default value for this parameter is 2, which means that objects nested more than two levels deep will be truncated. By increasing the value of this parameter, you can allow the ConvertTo-Json command to serialize objects that are more deeply nested.

Here's an example of how to increase the depth of serialization in PowerShell:

const { exec } = require('child_process');

exec('pwsh -Command "Get-ChildItem / | ConvertTo-Json -Depth 10"', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  const files = JSON.parse(stdout);
  console.log(files);
});

In this example, we are setting the -Depth parameter to 10 to allow for serialization of objects nested up to 10 levels deep. You can adjust this value as needed for your specific use case.

Now let's get to running external scripts. You can execute an external PowerShell script using the pwsh command in Node.js. Here's an example:

const { spawn } = require('child_process');

const scriptPath = '/path/to/script.ps1';

const ps = spawn('pwsh', [scriptPath]);

ps.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ps.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ps.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

In this example, we are using the spawn() function from the child_process module to execute an external PowerShell script located at /path/to/script.ps1. The pwsh command is passed as the first argument to spawn(), and the path to the script file is passed as an array of arguments.

The stdout and stderr streams are captured using the on() method, and the close event is used to log the exit code of the PowerShell script.

Now let's see an example for passing parameters. You can pass named parameters to an external PowerShell script from Node.js by including the parameter names and values as separate elements in the array passed as the second argument to the spawn() function.

Here's an example:

const { spawn } = require('child_process');

const scriptPath = '/path/to/script.ps1';
const parameter1 = 'value1';
const parameter2 = 'value2';

const ps = spawn('pwsh', [scriptPath, '-Parameter1', parameter1, '-Parameter2', parameter2]);

ps.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ps.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ps.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

In this example, we are passing two named parameters (-Parameter1 and -Parameter2) to the external PowerShell script located at /path/to/script.ps1. The pwsh command is passed as the first argument to spawn(), and the script path and named parameter names and values are passed as additional elements in the array.

Now for our final example, let's run an advanced function and pass parameters to it.

const { exec } = require('child_process');

// Define the PowerShell script file path
const scriptPath = '/path/to/MyScript.ps1';

// Define the function name and arguments
const functionName = 'MyFunction';
const functionArgs = '-Parameter1 "Value1" -Parameter2 "Value2"';

// Execute the PowerShell script and capture its output
exec(`pwsh -ExecutionPolicy Bypass -Command "& { . '${scriptPath}'; ${functionName} ${functionArgs} }"`, (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }

  // Output the function result
  console.log(`Function output: ${stdout}`);
});

And if you're on a Window machine:

const { exec } = require('child_process');

// Define the PowerShell script file path
const scriptPath = 'C:\\Scripts\\MyScript.ps1';

// Define the function name and arguments
const functionName = 'MyFunction';
const functionArgs = '-Parameter1 "Value1" -Parameter2 "Value2"';

// Execute the PowerShell script and capture its output
exec(`powershell.exe -ExecutionPolicy Bypass -Command "& { . '${scriptPath}'; ${functionName} ${functionArgs} }"`, (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }

  // Output the function result
  console.log(`Function output: ${stdout}`);
});

Not much is changed other than our script path and our executable(script path).

I know I didn't get to run scripts in sequences, but it feels like this post got longer than I intended. I'll tackle sequencing and more edge cases in upcoming tiny projects.

Did you find this article valuable?

Support Application Support by becoming a sponsor. Any amount is appreciated!