Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ the Github Actions Nightly workflow. It can only be run against `develop`.

## Synthetic mock daemons (UI development)

To run the UI without native client daemons, start the HTTP mock servers in [`synthetic-daemons/`](synthetic-daemons/) and launch Electros with `--no-daemons`. See [synthetic-daemons/README.md](synthetic-daemons/README.md) for ports, fixtures, and verification steps.
To run the UI with HTTP mock servers instead of native client daemons, use `--synthetic-daemons` so Electros starts [`synthetic-daemons/`](synthetic-daemons/) via `npm start` alongside the GUI. See [synthetic-daemons/README.md](synthetic-daemons/README.md) for ports, fixtures, and verification steps.

```bash
cd synthetic-daemons && npm install # once
cd electros-electron && npm start -- --synthetic-daemons
```

From the **Developer** menu you can switch at runtime between **Use Native Daemons** (`CmdOrCtrl+Shift+Alt+N`) and **Use Synthetic Daemons** (`CmdOrCtrl+Shift+Alt+S`) in unpackaged builds or when `--enable-devtools` is set.

To run mocks in a separate terminal instead:

```bash
cd synthetic-daemons && npm install && npm start
Expand All @@ -34,7 +43,8 @@ cd electros-electron && npm start -- --no-daemons
Electros has a set of custom command line switches other than the Electron switches:

- `--enable-devtools` enables the devtools for the application
- `--no-daemons` disables the execution of the embedded daemons (use with synthetic-daemons above)
- `--synthetic-daemons` launches [`synthetic-daemons/`](synthetic-daemons/) via `npm start` instead of the native client daemons (unpackaged/dev only)
- `--no-daemons` disables the execution of the embedded daemons (use when running synthetic-daemons yourself)

> [!NOTE]
> On macOS, to add a CLI switch, you'll have to append them directly onto the executable:
Expand Down
149 changes: 139 additions & 10 deletions electros-electron/common/Daemons.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,154 @@ export class DaemonsNotEnabledError extends Error {
export class Daemons {
static _Process = null;
static _Ports = {};
static _IsSynthetic = false;
static _ElectronDir = null;
static _Platform = null;
static _SwitchSettleMs = 750;

static _DaemonsLogArray = [];
static BUFFER_SIZE = 2000;

static DataUpdateCriticalHook = null;

static Launch(platform, __dirname) {
if (app.commandLine.hasSwitch("no-daemons")) {
/**
* @param {object} platform
* @param {string} __dirname
* @param {{ synthetic?: boolean }} [options]
*/
static Launch(platform, __dirname, options = {}) {
Daemons._ElectronDir = __dirname;
Daemons._Platform = platform;

if (app.commandLine.hasSwitch("no-daemons") && !options.synthetic) {
console.log("Daemons have been disabled by `--no-daemons`");
Terminal.Write("[INFO] Elemento Client Daemons have been disabled by `--no-daemons`.");
throw new DaemonsNotEnabledError();
}

const useSynthetic = options.synthetic === true
|| app.commandLine.hasSwitch("synthetic-daemons");

if (useSynthetic) {
Daemons._LaunchSynthetic(__dirname);
return;
}

Daemons._LaunchNative(platform, __dirname);
}

/**
* Switch to synthetic-daemons via `npm start` (Developer menu).
* Terminates any existing daemon process first.
* @param {string} [electronDir]
*/
static async LaunchSynthetic(electronDir) {
const dir = electronDir || Daemons._ElectronDir;
if (!dir) {
throw new Error("LaunchSynthetic requires electronDir (call Daemons.Launch first, or pass __dirname)");
}
await Daemons._SwitchTo(() => Daemons._LaunchSynthetic(dir));
}

/**
* Switch to native client daemons (Developer menu).
* Terminates any existing daemon process first; ignores --no-daemons / --synthetic-daemons.
* @param {object} [platform]
* @param {string} [electronDir]
*/
static async LaunchNative(platform, electronDir) {
const dir = electronDir || Daemons._ElectronDir;
const plat = platform || Daemons._Platform;
if (!dir || !plat) {
throw new Error("LaunchNative requires platform and electronDir (call Daemons.Launch first)");
}
await Daemons._SwitchTo(() => Daemons._LaunchNative(plat, dir));
}

static IsSynthetic() {
return Daemons._IsSynthetic;
}

static IsRunning() {
return Daemons._Process !== null && !Daemons._Process.killed;
}

static async _SwitchTo(launchFn) {
Daemons.Terminate();
// Allow ports to be released before binding the other daemon set
await new Promise((resolve) => setTimeout(resolve, Daemons._SwitchSettleMs));
launchFn();
}

static _LaunchNative(platform, __dirname) {
if (!app.isPackaged) {
Terminal.Write("[WARN] Electros is not packaged. Daemons might have to be manually started.");
console.warn("Electros is not packaged. Daemons might have to be manually started.");
}

const execPath = Daemons._GetCommand(platform, __dirname);
if (!execPath) {
const msg = "[ERROR] Native daemon binary not found for this platform.";
console.error(msg);
Terminal.Write(msg);
throw new Error(msg);
}

Terminal.Write(`[INFO] Starting native client daemons (${execPath})`);
console.log("Launching native daemons from", execPath);
console.trace(execPath);

Daemons._Process = spawn(
execPath, [], {
env: {...process.env, GUI_APP: '1'},
stdio: ['pipe', 'pipe', 'pipe'],
detached: false
}
);
Daemons._ElectronDir = __dirname;
Daemons._Platform = platform;
Daemons._IsSynthetic = false;
Daemons._SpawnProcess(execPath, [], {
env: {...process.env, GUI_APP: '1'},
stdio: ['pipe', 'pipe', 'pipe'],
detached: false
});
}

static _LaunchSynthetic(__dirname) {
if (app.isPackaged) {
const msg = "[ERROR] Synthetic daemons are only available in unpackaged (dev) builds.";
console.error(msg);
Terminal.Write(msg);
throw new Error("Synthetic daemons unavailable in packaged builds");
}

const syntheticRoot = Daemons._GetSyntheticPath(__dirname);
const packageJson = path.join(syntheticRoot, 'package.json');
if (!fs.existsSync(packageJson)) {
const msg = `[ERROR] synthetic-daemons not found at ${syntheticRoot}`;
console.error(msg);
Terminal.Write(msg);
throw new Error(msg);
}

Terminal.Write(`[INFO] Starting synthetic-daemons via npm start (${syntheticRoot})`);
console.log("Launching synthetic-daemons from", syntheticRoot);

const isWin = process.platform === 'win32';
const npmCmd = isWin ? 'npm.cmd' : 'npm';

Daemons._ElectronDir = __dirname;
Daemons._IsSynthetic = true;
Daemons._SpawnProcess(npmCmd, ['start'], {
cwd: syntheticRoot,
env: {...process.env, GUI_APP: '1'},
stdio: ['pipe', 'pipe', 'pipe'],
shell: isWin,
// New process group on Unix so Terminate can kill npm + node children
detached: !isWin,
});
}

static _GetSyntheticPath(electronDir) {
return path.join(electronDir, '..', 'synthetic-daemons');
}

static _SpawnProcess(command, args, spawnOptions) {
Daemons._Process = spawn(command, args, spawnOptions);

Daemons._Process.stdout.on("data", (data) => {
this._DaemonsLogArray.push(data);
Expand Down Expand Up @@ -75,12 +195,22 @@ export class Daemons {
execSync(`taskkill /pid ${Daemons._Process.pid} /T /F`);
} catch (e) {
console.error("Failed to kill process with taskkill:", e);
r = false;
}
} else if (Daemons._IsSynthetic && Daemons._Process.pid) {
// Kill the whole process group (npm + node dist/index.js)
try {
process.kill(-Daemons._Process.pid, 'SIGTERM');
} catch (e) {
console.error("Failed to kill synthetic-daemons process group:", e);
r = Daemons._Process.kill();
}
} else {
r = Daemons._Process.kill();
}
}
Daemons._Process = null;
Daemons._IsSynthetic = false;
}

return r;
Expand Down Expand Up @@ -153,4 +283,3 @@ export class Daemons {
return daemonsCmd;
}
}

57 changes: 40 additions & 17 deletions electros-electron/common/MenuBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { Terminal } from "../windows/Terminal.js";
import {app, Notification} from "electron";


function notify(title, body, urgency = 'low') {
if (!Notification.isSupported()) {
return;
}
new Notification({title, body, silent: true, urgency}).show();
}


export function BuildMenuTemplate() {
const baseMenu = [
{
Expand Down Expand Up @@ -75,31 +83,46 @@ export function BuildMenuTemplate() {
baseMenu.push({
label: 'Developer',
submenu:[
{
label: 'Use Native Daemons',
accelerator: 'CmdOrCtrl+Shift+Alt+N',
click: async () => {
console.log("switch to native daemons triggered");
try {
await Daemons.LaunchNative();
notify("Native Daemons Started", "Switched to native client daemons.");
} catch (e) {
console.error("Failed to launch native daemons:", e);
notify("Failed to Launch Native Daemons", e?.message || "Could not start native daemons.", 'normal');
}
}
},
{
label: 'Use Synthetic Daemons',
accelerator: 'CmdOrCtrl+Shift+Alt+S',
click: async () => {
console.log("switch to synthetic daemons triggered");
try {
await Daemons.LaunchSynthetic();
notify("Synthetic Daemons Started", "Switched to synthetic-daemons (npm start).");
} catch (e) {
console.error("Failed to launch synthetic daemons:", e);
notify("Failed to Launch Synthetic Daemons", e?.message || "Could not start synthetic-daemons.", 'normal');
}
}
},
{
label: 'Terminate Daemons',
click: async () => {
console.log("manual daemon termination triggered");
if(Daemons.Terminate()) {
if(Notification.isSupported()) {
new Notification({
title: "Daemons Terminated",
body: "Electros Client Daemons successfully terminated.",
silent: true,
urgency: 'low'
}).show();
}
if (Daemons.Terminate()) {
notify("Daemons Terminated", "Electros Client Daemons successfully terminated.");
} else {
if(Notification.isSupported()) {
new Notification({
title: "Failed to Terminate Daemons",
body: "Electros Client Daemons were not terminated.",
silent: true,
urgency: 'low'
}).show();
}
notify("Failed to Terminate Daemons", "Electros Client Daemons were not terminated.", 'low');
}
}
},
{type: 'separator'},
{label: 'Toggle DevTools', role: 'toggleDevTools'},
{label: 'Toggle Fullscreen', role: 'toggleFullScreen'},
]
Expand Down
6 changes: 3 additions & 3 deletions electros-electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function createMainWindow() {
}

// Inject custom titlebar after the page loads
win.webContents.once('did-finish-load', () => {
win.webContents.on('did-finish-load', () => {
try {
const safeJS = typeof PreloadedContent.Js.Titlebar === 'string'
? PreloadedContent.Js.Titlebar
Expand Down Expand Up @@ -172,7 +172,7 @@ ipcMain.handle('create-popup', async (event, options = {}) => {
try {
// Inject custom titlebar CSS and HTML before loading the URL
if (!options.defaultTitlebar) {
popup.webContents.once('did-finish-load', () => {
popup.webContents.on('did-finish-load', () => {
const popupTitlebarJS = PreloadedContent.Js.Titlebar.replace(
'titleElement.textContent = document.title;',
`titleElement.textContent = ${JSON.stringify(options.title)};`
Expand Down Expand Up @@ -397,7 +397,7 @@ ipcMain.handle('open-ssh', async (event, connectionDetails) => {

event.sender.ssh_port = ssh_port;

sshWindow.webContents.once('did-finish-load', () => {
sshWindow.webContents.on('did-finish-load', () => {
const sshTitlebarJS = PreloadedContent.Js.Titlebar.replace(
'titleElement.textContent = document.title;',
`titleElement.textContent = "SSH connection to ${connectionDetails.vmName}";`
Expand Down
2 changes: 1 addition & 1 deletion electros-electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"scripts": {
"synthetic-daemons": "npm run start --prefix ../synthetic-daemons",
"start": "concurrently --kill-others --names \"vite,electron\" -c \"green,yellow\" -l 10 --pad-prefix \"vite\" \"electron . --trace-warnings --v=1\"",
"start": "concurrently --kill-others --names \"vite,electron\" -c \"green,yellow\" -l 10 --pad-prefix -P \"vite\" \"electron . --trace-warnings --v=1 {@}\" --",
"dev": "vite",
"build": "node build.mjs",
"build:nosign": "node build.mjs --config.mac.identity=null",
Expand Down
2 changes: 1 addition & 1 deletion elemento-gui-new
15 changes: 14 additions & 1 deletion synthetic-daemons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ Standalone mock servers that emulate Elemento **client daemons** on the ECD loca

## Quick start

One-command demo (Electros spawns this package via `npm start`):

```bash
cd synthetic-daemons && npm install # once
cd ../electros-electron
npm start -- --synthetic-daemons
```

Or from the Electros **Developer** menu: **Use Synthetic Daemons** (`CmdOrCtrl+Shift+Alt+S`) / **Use Native Daemons** (`CmdOrCtrl+Shift+Alt+N`) to switch at runtime.

### Manual (two terminals)

```bash
cd synthetic-daemons
npm install
Expand Down Expand Up @@ -130,5 +142,6 @@ npm run build # compile only
From `electros-electron`:

```bash
npm run synthetic-daemons
npm start -- --synthetic-daemons # GUI + mocks together
npm run synthetic-daemons # mocks only
```
Loading