在Electron中运行C++服务器

huangapple go评论76阅读模式
英文:

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

<details>
<summary>英文:</summary>

I created an application with electron and react. This application has an internal c++ server. In dev modality I don&#39;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, &#39;../../server&#39;)
        : join(process.env.DIST_ELECTRON, &#39;../server&#39;);

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

      server.stdout.on(&#39;data&#39;, (data) =&gt; {
        console.log(`Server stdout: ${data}`);
      });

      server.stderr.on(&#39;data&#39;, (data) =&gt; {
        console.error(`Server stderr: ${data}`);
      });

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

where:

    process.env.DIST_ELECTRON = join(__dirname, &#39;../&#39;);
    process.env.DIST = join(process.env.DIST_ELECTRON, &#39;../dist&#39;);
    process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL ? 
                              join(process.env.DIST_ELECTRON, &#39;../public&#39;) : 
                              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
     */
    {
      &quot;appId&quot;: &quot;it.softwaves.dial-designer&quot;,
      &quot;productName&quot;: &quot;Dial Designer&quot;,
      &quot;asar&quot;: true,  
      &quot;directories&quot;: {
        &quot;output&quot;: &quot;release/${version}&quot;
      },
      &quot;files&quot;: [
        &quot;dist-electron&quot;,
        &quot;dist&quot;,
        &quot;server&quot;
      ],
      &quot;mac&quot;: {
        &quot;artifactName&quot;: &quot;Dial_Designer_${version}.${ext}&quot;,
        &quot;target&quot;: [
          &quot;dmg&quot;,
          &quot;zip&quot;
        ]
      },
      &quot;win&quot;: {
        &quot;target&quot;: [
          {
            &quot;target&quot;: &quot;nsis&quot;,
            &quot;arch&quot;: [
              &quot;x64&quot;
            ]
          }
        ],
        &quot;artifactName&quot;: &quot;Dial_Designer_${version}.${ext}&quot;
      },
      &quot;nsis&quot;: {
        &quot;oneClick&quot;: false,
        &quot;perMachine&quot;: false,
        &quot;allowToChangeInstallationDirectory&quot;: true,
        &quot;deleteAppDataOnUninstall&quot;: false
      },
      &quot;publish&quot;: {
        &quot;provider&quot;: &quot;generic&quot;,
        &quot;channel&quot;: &quot;latest&quot;,
        &quot;url&quot;: &quot;https://github.com/electron-vite/electron-vite-react/releases/download/v0.9.9/&quot;
      }
    }


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

    import { join, dirname } from &#39;node:path&#39;;
    import { release } from &#39;node:os&#39;;
    import { spawn } from &#39;node:child_process&#39;;
    import { copySync } from &#39;fs-extra&#39;;
    
    import { 
      app, BrowserWindow, screen, 
      shell, Menu, MenuItemConstructorOptions, dialog 
    } from &#39;electron&#39;;
    
    import { update } from &#39;./update&#39;;
    
    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 === &#39;darwin&#39;;
    
      constructor() {
        process.env.DIST_ELECTRON = join(__dirname, &#39;../&#39;);
        process.env.DIST = join(process.env.DIST_ELECTRON, &#39;../dist&#39;);
        process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL ? 
                                  join(process.env.DIST_ELECTRON, &#39;../public&#39;) : 
                                  process.env.DIST;
    
        DialDesigner.url = process.env.VITE_DEV_SERVER_URL;
        DialDesigner.indexHtml = join(process.env.DIST, &#39;index.html&#39;);
    
        if (release().startsWith(&#39;6.1&#39;)) app.disableHardwareAcceleration();
        if (process.platform === &#39;win32&#39;) app.setAppUserModelId(app.getName());
    
        if (!app.requestSingleInstanceLock()) {
          app.quit();
          process.exit(0);
        }
      }
    
      public static newWindow = (win: IWin): BrowserWindow =&gt; {
        return new BrowserWindow({
          title: win.title,
          icon: join(process.env.PUBLIC, &#39;favicon.ico&#39;),
          width: win.width,
          height: win.height,
          center: true,
          resizable: win.resizable ?? true, 
          webPreferences: {
            preload: win?.preload,
            nodeIntegration: true,
            contextIsolation: false,
          },
        });
      }
    
      private runServer = () =&gt; {
        let appPath = null;
        try {
          appPath = app.isPackaged
            ? join(process.env.DIST, &#39;../server&#39;)
            : join(process.env.DIST_ELECTRON, &#39;../server&#39;);
    
          const server = spawn(
            join(appPath, &#39;operations&#39;),
            [
              join(appPath, &#39;settings.json&#39;),
              join(appPath, &#39;../public/tmp&#39;),
            ],
            { cwd: appPath }
          );
    
          server.stdout.on(&#39;data&#39;, (data) =&gt; {
            console.log(`Server stdout: ${data}`);
          });
    
          server.stderr.on(&#39;data&#39;, (data) =&gt; {
            console.error(`Server stderr: ${data}`);
          });
    
          server.on(&#39;close&#39;, (code) =&gt; {
            console.log(`Server process exited with code ${code}`);
          });
        }
        catch (err) {
          dialog.showMessageBox(DialDesigner.main!, { title: &#39;Errore&#39;, message: 
            `PUBLIC: ${process.env.PUBLIC}
             DIST: ${process.env.DIST}
             EXE: ${join(appPath!, &#39;operations&#39;)}
            `
            });
          app.quit();
        }
      }
    
      private createWindow = () =&gt; {
        const { width, height } = screen.getPrimaryDisplay().size;
    
        DialDesigner.main = DialDesigner.newWindow({ 
          title: &#39;Dial Designer&#39;,
          width, height,
          preload: join(__dirname, &#39;../preload/index.js&#39;),
        });
    
        if (DialDesigner.url) {
          DialDesigner.main.loadURL(DialDesigner.url);
          DialDesigner.main.webContents.openDevTools();
        }
        else {
          DialDesigner.main.loadFile(DialDesigner.indexHtml);
        }
    
        DialDesigner.main.webContents.on(&#39;did-finish-load&#39;, () =&gt; {
          DialDesigner.main?.webContents.send(&#39;main-process-message&#39;, new Date().toLocaleString())
        });
      
        // Make all links open with the browser, not with the application
        DialDesigner.main.webContents.setWindowOpenHandler(({ url }) =&gt; {
          if (url.startsWith(&#39;https:&#39;)) shell.openExternal(url)
          return { action: &#39;deny&#39; }
        });
    
        update(DialDesigner.main);
      }
    
      public initialize = () =&gt; {
        try {
          app.setPath(&#39;userData&#39;, `${app.getPath(&#39;appData&#39;)}/Dial Designer`);
    
          app.whenReady()
            .then(this.createWindow);
    
          app.on(&#39;ready&#39;, this.runServer);
    
          app.on(&#39;window-all-closed&#39;, () =&gt; {
            DialDesigner.main = null
            if (process.platform !== &#39;darwin&#39;) app.quit()
          });
    
          app.on(&#39;second-instance&#39;, () =&gt; {
            if (DialDesigner.main) {
              if (DialDesigner.main.isMinimized()) DialDesigner.main.restore();
              DialDesigner.main.focus();
            }
          });
    
          app.on(&#39;activate&#39;, () =&gt; {
            const allWindows = BrowserWindow.getAllWindows()
            if (allWindows.length) allWindows[0].focus();
            else this.createWindow();
          });
        }
        catch (err) {
          dialog.showMessageBox(DialDesigner.main!, { title: &#39;Errore&#39;, message: `${(err as Error).stack}` });
          app.quit();
        }
      }
    
      public applicationMenu = (template: MenuItemConstructorOptions[]) =&gt; {
        const menu = Menu.buildFromTemplate(template);
        Menu.setApplicationMenu(menu);
      }
    }



</details>


# 答案1
**得分**: 1

我怀疑这是由于打包和未打包路径不同而导致的错误路径引起的

**跟踪问题的步骤**

1. 打开打包后的应用程序目录找到你的 `app.asar` 文件
2. 使用 `npx @electron/asar extract app.asar appAsar` 进行解压
3. 确保你的预加载脚本位于预期的目录中 - 很可能由于打包的结果它位于一个子目录中因此你需要修复路径以适应应用程序是打包还是未打包的情况

例如在我们的代码中我们使用类似以下的代码

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

如果仍然无法解决问题 - 通过在你的 mainpreload 进程脚本中添加以下代码来添加 electron-unhandled 到你的应用程序中:

import unhandled from 'electron-unhandled';

unhandled();

确保还运行了 npm install --save electron-unhandled

这应该会显示错误对话框,显示出现的错误(如果有的话)。

英文:

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 ? &#39;dist&#39; : &#39;&#39;,
  &#39;preload.build.js&#39;
);

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 &#39;electron-unhandled&#39;;

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.

huangapple
  • 本文由 发表于 2023年6月9日 00:30:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/76433966.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定