How do I correctly launch a shell environment with node-pty in Electron?

957 Views Asked by At

I have an Electron App that's using node-pty to create a pseudo-terminal instance. This instance is created as follows:

const pty = require('node-pty');
const os = require('os');
const shell = process.env[os.platform() === 'win32' ? 'COMSPEC' : 'SHELL'];
const ptyProcess = pty.spawn(shell, [], {
  name: 'xterm-color',
  cols: 100,
  rows: 40,
  cwd: process.env.HOME,
  env: process.env
}); 

When I launch the app from the vscode terminal using (electron .) everything works as expected. I.e. the shell can find all programs just like it would in cmd.exe or Terminal.app.

However, if I build the app, and open it via double clicking on it, I notice that my path does not contain certain directories such as /usr/local/bin/, which can cause certain programs (like brew) to fail with "Not found" errors.

Notably, if I launch the electron app from a terminal on mac using open -a MyElectronApp everything works as expected.

I suspect my app needs to initialize the path differently somehow. However, since this is a cross-platform app, I'd like to avoid doing anything like "if os.platform == 'darwin' load /etc/paths".

Any help would be greatly appreciated. Please let me know if you need more information. I assume my issue is probably due to a misunderstanding/ignorance of shell environments!

2

There are 2 best solutions below

1
On BEST ANSWER

Faced same issue. Used https://github.com/sindresorhus/fix-path

process.env.PATH was not available for the electron process in production mode. The above fix-path fixes the issue

0
On

I suspect the problem is a bit deeper than just PATH, and "fix-path" is just a band-aid.

To illustrate the problem, let's say you're using Zsh, which is the default on macOS. Zsh uses several startup files, the most notable of which are:

  • .zprofile, which is run for login shells

  • .zshrc, which is run for interactive shells

Where this can trip you up is that your PATH may be being set in .zprofile.

This is normally not a problem when you launch a shell in a terminal, because macOS Terminal.app runs it as a login shell.

But when you are just invoking /bin/zsh with pty.spawn, you're only creating an interactive shell. That means .zshrc will get sourced, but .zprofile won't.

To make node-pty launch zsh as a login shell, you have two options:

  1. You can pass the "--login" flag, if your shell supports it. Zsh and Bash both do. This is what Visual Studio Code does.

  2. You can set argv[0] to the name of the shell prefixed by a hyphen, e.g. "-zsh". This effectively tells the spawned process that its name is "-zsh" instead of "/bin/zsh" or similar, and—most crucially—is the accepted and most universal way on Unix systems to indicate the shell should be a login shell.

The latter option is not yet supported in node-pty releases as of this writing, but I submitted a PR adding this support which is tagged for review for a future version.

I am using my patched version in a personal project:

{
  "dependencies": {
    "node-pty": "github:mattieb/node-pty#add-argv0-support"
  }
}

This makes a new option, "argv0", available, to which you can set a hyphen followed by the base name of your shell:

import { basename } from "node:path";
import { spawn } from "node-pty";

const file = process.env.SHELL ?? "/bin/sh";
const argv0 = `-${basename(file)}`;
const pty = spawn("/bin/zsh", [], { argv0 });

With both strategies, you simply don't need to pass "process.env" as the environment to spawn. Instead, the shell's own startup will set the environment.