Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run C++ server in Electron

I created an application with electron and react. This application has an internal c++ server. In dev modality I don't have problems but after packaging the server not starts.

This is my folder tree:

├── dist-electron
│   ├── main
│   └── preload
├── electron
│   ├── main
│   └── preload
├── public
│   └── tmp
├── server
└── src
    ├── @types
    ├── api
    ├── assets
    ├── common
    ├── components
    ├── context
    ├── routes
    └── theme

The server folder is included after react build into the dist folder.

In the electron main i wrote this function:

try {
  const appPath = app.isPackaged
    ? join(process.env.PUBLIC, '../../server')
    : join(process.env.DIST_ELECTRON, '../server');

  const server = spawn(
    join(appPath, 'operations'),
    [
      join(appPath, 'settings.json'),
      join(appPath, '../public/tmp'),
    ],
    { cwd: appPath }
  );

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

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

  server.on('close', (code) => {
    console.log(`Server process exited with code ${code}`);
  });
}
catch (err) {
  dialog.showMessageBox(DialDesigner.main!, { title: 'Errore', message: process.env.PUBLIC });
  app.quit();
}

where:

process.env.DIST_ELECTRON = join(__dirname, '../');
process.env.DIST = join(process.env.DIST_ELECTRON, '../dist');
process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL ? 
                          join(process.env.DIST_ELECTRON, '../public') : 
                          process.env.DIST;

I always have spawn ENOTDIR as error. I extract the app.asar file and the operations executable exists in the correct directory dist/server

This is the electron-builder.json5 configuration file:

/**
 * @see https://www.electron.build/configuration/configuration
 */
{
  "appId": "it.softwaves.dial-designer",
  "productName": "Dial Designer",
  "asar": true,  
  "directories": {
    "output": "release/${version}"
  },
  "files": [
    "dist-electron",
    "dist",
    "server"
  ],
  "mac": {
    "artifactName": "Dial_Designer_${version}.${ext}",
    "target": [
      "dmg",
      "zip"
    ]
  },
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": [
          "x64"
        ]
      }
    ],
    "artifactName": "Dial_Designer_${version}.${ext}"
  },
  "nsis": {
    "oneClick": false,
    "perMachine": false,
    "allowToChangeInstallationDirectory": true,
    "deleteAppDataOnUninstall": false
  },
  "publish": {
    "provider": "generic",
    "channel": "latest",
    "url": "https://github.com/electron-vite/electron-vite-react/releases/download/v0.9.9/"
  }
}

And this it the DialDesigner.ts class that instance and configure the electron application:

import { join, dirname } from 'node:path';
import { release } from 'node:os';
import { spawn } from 'node:child_process';
import { copySync } from 'fs-extra';

import { 
  app, BrowserWindow, screen, 
  shell, Menu, MenuItemConstructorOptions, dialog 
} from 'electron';

import { update } from './update';

interface IWin {
  title: string,
  width: number,
  height: number,
  preload?: any,
  resizable?: boolean,
}

export default class DialDesigner {

  public static main: BrowserWindow | null;
  public static settings: BrowserWindow | null;
  
  public static url: string | undefined;
  public static indexHtml: string;
  public static IS_MAC: boolean = process.platform === 'darwin';

  constructor() {
    process.env.DIST_ELECTRON = join(__dirname, '../');
    process.env.DIST = join(process.env.DIST_ELECTRON, '../dist');
    process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL ? 
                              join(process.env.DIST_ELECTRON, '../public') : 
                              process.env.DIST;

    DialDesigner.url = process.env.VITE_DEV_SERVER_URL;
    DialDesigner.indexHtml = join(process.env.DIST, 'index.html');

    if (release().startsWith('6.1')) app.disableHardwareAcceleration();
    if (process.platform === 'win32') app.setAppUserModelId(app.getName());

    if (!app.requestSingleInstanceLock()) {
      app.quit();
      process.exit(0);
    }
  }

  public static newWindow = (win: IWin): BrowserWindow => {
    return new BrowserWindow({
      title: win.title,
      icon: join(process.env.PUBLIC, 'favicon.ico'),
      width: win.width,
      height: win.height,
      center: true,
      resizable: win.resizable ?? true, 
      webPreferences: {
        preload: win?.preload,
        nodeIntegration: true,
        contextIsolation: false,
      },
    });
  }

  private runServer = () => {
    let appPath = null;
    try {
      appPath = app.isPackaged
        ? join(process.env.DIST, '../server')
        : join(process.env.DIST_ELECTRON, '../server');

      const server = spawn(
        join(appPath, 'operations'),
        [
          join(appPath, 'settings.json'),
          join(appPath, '../public/tmp'),
        ],
        { cwd: appPath }
      );

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

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

      server.on('close', (code) => {
        console.log(`Server process exited with code ${code}`);
      });
    }
    catch (err) {
      dialog.showMessageBox(DialDesigner.main!, { title: 'Errore', message: 
        `PUBLIC: ${process.env.PUBLIC}
         DIST: ${process.env.DIST}
         EXE: ${join(appPath!, 'operations')}
        `
        });
      app.quit();
    }
  }

  private createWindow = () => {
    const { width, height } = screen.getPrimaryDisplay().size;

    DialDesigner.main = DialDesigner.newWindow({ 
      title: 'Dial Designer',
      width, height,
      preload: join(__dirname, '../preload/index.js'),
    });

    if (DialDesigner.url) {
      DialDesigner.main.loadURL(DialDesigner.url);
      DialDesigner.main.webContents.openDevTools();
    }
    else {
      DialDesigner.main.loadFile(DialDesigner.indexHtml);
    }

    DialDesigner.main.webContents.on('did-finish-load', () => {
      DialDesigner.main?.webContents.send('main-process-message', new Date().toLocaleString())
    });
  
    // Make all links open with the browser, not with the application
    DialDesigner.main.webContents.setWindowOpenHandler(({ url }) => {
      if (url.startsWith('https:')) shell.openExternal(url)
      return { action: 'deny' }
    });

    update(DialDesigner.main);
  }

  public initialize = () => {
    try {
      app.setPath('userData', `${app.getPath('appData')}/Dial Designer`);

      app.whenReady()
        .then(this.createWindow);

      app.on('ready', this.runServer);

      app.on('window-all-closed', () => {
        DialDesigner.main = null
        if (process.platform !== 'darwin') app.quit()
      });

      app.on('second-instance', () => {
        if (DialDesigner.main) {
          if (DialDesigner.main.isMinimized()) DialDesigner.main.restore();
          DialDesigner.main.focus();
        }
      });

      app.on('activate', () => {
        const allWindows = BrowserWindow.getAllWindows()
        if (allWindows.length) allWindows[0].focus();
        else this.createWindow();
      });
    }
    catch (err) {
      dialog.showMessageBox(DialDesigner.main!, { title: 'Errore', message: `${(err as Error).stack}` });
      app.quit();
    }
  }

  public applicationMenu = (template: MenuItemConstructorOptions[]) => {
    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);
  }
}
like image 940
th3g3ntl3man Avatar asked Jun 08 '26 01:06

th3g3ntl3man


1 Answers

I suspect this is caused by a bad path resulting from packaged vs unpackaged paths differing.

To trace the issue:

  1. Open up the packaged app directory and find your app.asar
  2. Extract it using npx @electron/asar extract app.asar appAsar
  3. Ensure your preload script is in the expected directory - it's likely it's in a subdirectory as a result of packaging and that you need to patch the path to adjust for whether the app is packaged or not

For example, in our code we use something like this:

export const preloadPath = path.join(
  __dirname,
  IS_DEV_ENV ? 'dist' : '',
  'preload.build.js'
);

If that still doesn't work - add electron-unhandled to your application by adding these lines to your main and preload process scripts.

import unhandled from 'electron-unhandled';

unhandled();

Be sure to also run npm install --save electron-unhandled

This should show you a dialog box with the error that's occurring, if any.

like image 105
Slbox Avatar answered Jun 10 '26 16:06

Slbox