node-pty icon indicating copy to clipboard operation
node-pty copied to clipboard

Got Error: posix_spawnp failed in packaged electron js app

Open paulosabayomi opened this issue 7 months ago • 0 comments

Hi, I am using node-pty with xterm as the pseudo-terminal in the electron app I am working on, it works fine in development but when packaged it throws this error

Error: posix_spawnp failed

My ElectronJS environment is

  • ElectronForge
  • Vite
  • TypeScript

Also, I added node-pty to the build.rollupOptions.external option and I had to dynamically copy the files to the asar directory during packaging because vite didn't include the node-pty files during packaging, the following is the code I used to copy the node-pty files

const config: ForgeConfig = {
  //...
  },
  rebuildConfig: {},
  hooks: {
      // The call to this hook is mandatory for better-sqlite3 to work once the app built
      async packageAfterCopy(_forgeConfig, buildPath) {
          const requiredNativePackages = ['node-pty', 'es-git'];

          // __dirname isn't accessible from here
          const dirnamePath: string = ".";
          const sourceNodeModulesPath = path.resolve(dirnamePath, "node_modules");
          const destNodeModulesPath = path.resolve(buildPath, "node_modules");

          // Copy all asked packages in /node_modules directory inside the asar archive
          await Promise.all(
              requiredNativePackages.map(async (packageName) => {
                  const sourcePath = path.join(sourceNodeModulesPath, packageName);
                  const destPath = path.join(destNodeModulesPath, packageName);

                  await fs.mkdirs(path.dirname(destPath));
                  await fs.copy(sourcePath, destPath, {
                      recursive: true,
                      preserveTimestamps: true
                  });
              })
          );
      }
  },
  makers: [/** */],
  plugins: [
    //...
  ],
};

and this is my the node-pty implementation code

import * as cp from 'child_process';
import { ipcMain, WebContents } from 'electron';
import crypto from "crypto"
import nodepty, { IPty } from "node-pty"
import path from 'path'
import { app_logger } from '../server-actions/functions';
import fixPath from 'fix-path'
import fs from 'fs'
import { APP_HOME_DIRECTORY } from '../server-actions/server-constants';

fixPath()


const terminal_proc: Record<string, IPty> = {}

export const launchTerminal = async (webContents: WebContents, root_path: string) => {

    const terminal_proc_id: string = crypto.randomUUID();

    const default_shell = process.env.SHELL.split(path.sep).pop() || await getDefaultShell();

    try {
        terminal_proc[terminal_proc_id] = nodepty.spawn(default_shell, [], {
            name: 'xterm-color',
            cols: 80,
            rows: 30,
            cwd: root_path,
            env: process.env,
        });

           
        webContents.send("terminal-spawn-success", terminal_proc_id)
    } catch (error) {
      webContents.send("server-toast-broadcast-ev", {type: "random-toast", msg: "Error spawning terminal process: Reason: " + error.toString()})
    }
    try {
        terminal_proc_handler(webContents, terminal_proc[terminal_proc_id], terminal_proc_id);
        terminal_proc[terminal_proc_id].onExit((e) => {
          app_logger("error occured in the terminal_proc_handler onexit ", JSON.stringify(e))
          delete terminal_proc[terminal_proc_id]
        })
    } catch (error) {
        
    }
}

const terminal_proc_handler = (webContents: WebContents, terminal: IPty, proc_id: string) => {
    terminal.onData((data) => {
      webContents.send(`terminal-${proc_id}-ev`, {proc_id, chunk: data})
    });
    
    const client_message_listener = async (ev: any, data: any) => {
        if (data.resize) {
            return terminal.resize(data.resize.cols, data.resize.rows)
        }
        if (data.kill) {
            return terminal.kill()
        }
        terminal.write(data.data)
    }

    ipcMain.on(`terminal-${proc_id}-message`, client_message_listener)
}


const getDefaultShell = async (): Promise<string> => {
  if (process.platform === 'darwin') { // macOS
    try {
      const result = cp.execSync('dscl . read /Users/$USER UserShell').toString().trim();
      const shellPath = result.split(': ').pop() || '';
      const baseName = shellPath.split('/').pop();
      return baseName || shellPath || 'zsh'; // default to zsh on macOS
    } catch (error) {
      return 'zsh'; // Fallback to zsh
    }

  } else if (process.platform === 'linux') {
    try {
      const result = cp.execSync('getent passwd $USER | cut -d: -f7').toString().trim();
      const baseName = result.split('/').pop();
      return baseName || result || 'bash'; // default to bash on linux
    } catch (error) {
      return 'bash'; // Fallback to bash
    }
  } else if (process.platform === 'win32') {
    try {
        // first detect powershell
       if (process.env.PSModulePath) {
         return 'powershell.exe';
       }
       return 'cmd.exe';

    } catch (error) {
      return 'cmd.exe';
    }
  } else {
    return 'sh'; // Generic Unix shell
  }
}

Please what I'm I doing wrong, thank you

paulosabayomi avatar Jul 21 '25 01:07 paulosabayomi