Skip to content

Commit

Permalink
Add Windows compatibility (#30)
Browse files Browse the repository at this point in the history
* Add Windows compatibility for PID extraction and IPFS handling

* Update system platform detection and Windows compatibility

* Reversed the changes when calling getPid in the condition

* Remove redundant file existence check before deletion

* Ignore rm config failures

* Node 20

* Fix svg check

* Update lockfile

---------

Co-authored-by: Noisekit <[email protected]>
  • Loading branch information
vderunov and noisekit authored Feb 14, 2024
1 parent 2cc6eb3 commit 0eceae3
Show file tree
Hide file tree
Showing 8 changed files with 747 additions and 2,139 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2.1
parameters:
node-version:
type: string
default: '16.17.0'
default: '20.10.0'

commands:
npm-install:
Expand Down
2,723 changes: 631 additions & 2,092 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
"build:main": "NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
"build:renderer": "NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
"config:check": "ts-node ./src/config-check.ts ./config.json",
"lint": "eslint --fix .",
"lint:check": "eslint .",
"pretty": "prettier --write . './**/*.svg'",
"pretty:check": "prettier --check . './**/*.svg'",
"svg": "svgo --recursive . && prettier --write './**/*.svg'",
"svg:check": "npm run svg && git diff --exit-code",
"svg:check": "npm run svg && git diff --exit-code **/*.svg",
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder build --publish never",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
"start:main": "NODE_ENV=development electronmon -r ts-node/register/transpile-only .",
"start:preload": "NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:renderer": "NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"start:main": "cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only .",
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"iconsgen": "node ./assets/iconsgen.js ./assets/icon.svg ./assets"
},
"browserslist": [],
Expand All @@ -41,6 +41,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tar": "^6.1.13",
"unzipper": "^0.10.14",
"viem": "^0.3.1",
"zod": "^3.21.4"
},
Expand All @@ -55,11 +56,13 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/tar": "^6.1.4",
"@types/unzipper": "^0.10.9",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"browserslist-config-erb": "^0.0.3",
"concurrently": "^8.0.1",
"core-js": "^3.27.2",
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"detect-port": "^1.5.1",
"electron": "^24.1.1",
Expand Down
43 changes: 33 additions & 10 deletions src/main/follower.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ import logger from 'electron-log';
import { SYNTHETIX_IPNS } from '../const';
import { ROOT } from './settings';
import { getPid, getPidsSync } from './pid';
import unzipper from 'unzipper';
import { getPlatformDetails } from './util';

// Change if we ever want to store all follower info in a custom folder
const HOME = os.homedir();
const IPFS_FOLLOW_PATH = path.join(HOME, '.ipfs-cluster-follow');

export function followerKill() {
try {
getPidsSync('.synthetix/ipfs-cluster-follow/ipfs-cluster-follow').forEach((pid) => {
getPidsSync(
process.platform === 'win32'
? 'ipfs-cluster-follow.exe'
: '.synthetix/ipfs-cluster-follow/ipfs-cluster-follow synthetix run'
).forEach((pid) => {
logger.log('Killing ipfs-cluster-follow', pid);
process.kill(pid);
});
Expand All @@ -28,12 +34,24 @@ export function followerKill() {
}

export async function followerPid() {
return await getPid('.synthetix/ipfs-cluster-follow/ipfs-cluster-follow synthetix run');
return await getPid(
process.platform === 'win32'
? 'ipfs-cluster-follow.exe'
: '.synthetix/ipfs-cluster-follow/ipfs-cluster-follow synthetix run'
);
}

export async function followerIsInstalled() {
try {
await fs.access(path.join(ROOT, 'ipfs-cluster-follow/ipfs-cluster-follow'), fs.constants.F_OK);
await fs.access(
path.join(
ROOT,
process.platform === 'win32'
? 'ipfs-cluster-follow/ipfs-cluster-follow.exe'
: 'ipfs-cluster-follow/ipfs-cluster-follow'
),
fs.constants.F_OK
);
return true;
} catch (_e) {
return false;
Expand All @@ -45,7 +63,13 @@ export async function followerDaemon() {
if (!isInstalled) {
return;
}
const pid = await getPid('.synthetix/ipfs-cluster-follow/ipfs-cluster-follow synthetix run');

const pid = await getPid(
process.platform === 'win32'
? 'ipfs-cluster-follow.exe'
: '.synthetix/ipfs-cluster-follow/ipfs-cluster-follow synthetix run'
);

if (!pid) {
await configureFollower();

Expand Down Expand Up @@ -120,8 +144,7 @@ export async function getInstalledVersion() {
}

export async function downloadFollower(_e?: IpcMainInvokeEvent, { log = logger.log } = {}) {
const arch = os.arch();
const targetArch = arch === 'x64' ? 'amd64' : 'arm64';
const { osPlatform, fileExt, targetArch } = getPlatformDetails();

log('Checking for existing ipfs-cluster-follow installation...');

Expand All @@ -140,18 +163,18 @@ export async function downloadFollower(_e?: IpcMainInvokeEvent, { log = logger.l
log(`Installing ipfs-cluster-follow version ${latestVersionNumber}`);
}

const downloadUrl = `https://dist.ipfs.tech/ipfs-cluster-follow/${latestVersion}/ipfs-cluster-follow_${latestVersion}_darwin-${targetArch}.tar.gz`;
const downloadUrl = `https://dist.ipfs.tech/ipfs-cluster-follow/${latestVersion}/ipfs-cluster-follow_${latestVersion}_${osPlatform}-${targetArch}.${fileExt}`;
log(`ipfs-cluster-follow package: ${downloadUrl}`);

await fs.mkdir(ROOT, { recursive: true });
await new Promise((resolve, reject) => {
const file = createWriteStream(path.join(ROOT, 'ipfs-cluster-follow.tar.gz'));
const file = createWriteStream(path.join(ROOT, `ipfs-cluster-follow.${fileExt}`));
https.get(downloadUrl, (response) => pipeline(response, file).then(resolve).catch(reject));
});

await new Promise((resolve, reject) => {
createReadStream(path.join(ROOT, 'ipfs-cluster-follow.tar.gz'))
.pipe(zlib.createGunzip())
createReadStream(path.join(ROOT, `ipfs-cluster-follow.${fileExt}`))
.pipe(fileExt === 'zip' ? unzipper.Extract({ path: ROOT }) : zlib.createGunzip())
.pipe(tar.extract({ cwd: ROOT }))
.on('error', reject)
.on('end', resolve);
Expand Down
32 changes: 21 additions & 11 deletions src/main/ipfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ import type { IpcMainInvokeEvent } from 'electron';
import { getPid, getPidsSync } from './pid';
import { ROOT } from './settings';
import logger from 'electron-log';
import unzipper from 'unzipper';
import { getPlatformDetails } from './util';

const HOME = os.homedir();
// Change if we ever want IPFS to store its data in non-standart path
const IPFS_PATH = path.join(HOME, '.ipfs');

export function ipfsKill() {
try {
getPidsSync('.synthetix/go-ipfs/ipfs').forEach((pid) => {
getPidsSync(
process.platform === 'win32' ? 'ipfs.exe' : '.synthetix/go-ipfs/ipfs daemon'
).forEach((pid) => {
logger.log('Killing ipfs', pid);
process.kill(pid);
});
Expand All @@ -30,12 +34,15 @@ export function ipfsKill() {
}

export async function ipfsPid() {
return await getPid('.synthetix/go-ipfs/ipfs daemon');
return await getPid(process.platform === 'win32' ? 'ipfs.exe' : '.synthetix/go-ipfs/ipfs daemon');
}

export async function ipfsIsInstalled() {
try {
await fs.access(path.join(ROOT, 'go-ipfs/ipfs'), fs.constants.F_OK);
await fs.access(
path.join(ROOT, process.platform === 'win32' ? 'go-ipfs/ipfs.exe' : 'go-ipfs/ipfs'),
fs.constants.F_OK
);
return true;
} catch (_e) {
return false;
Expand All @@ -47,7 +54,11 @@ export async function ipfsDaemon() {
if (!isInstalled) {
return;
}
const pid = await getPid('.synthetix/go-ipfs/ipfs daemon');

const pid = await getPid(
process.platform === 'win32' ? 'ipfs.exe' : '.synthetix/go-ipfs/ipfs daemon'
);

if (!pid) {
await configureIpfs();
spawn(path.join(ROOT, 'go-ipfs/ipfs'), ['daemon'], {
Expand Down Expand Up @@ -104,8 +115,7 @@ export async function getInstalledVersion() {
}

export async function downloadIpfs(_e?: IpcMainInvokeEvent, { log = logger.log } = {}) {
const arch = os.arch();
const targetArch = arch === 'x64' ? 'amd64' : 'arm64';
const { osPlatform, fileExt, targetArch } = getPlatformDetails();

log('Checking for existing ipfs installation...');

Expand All @@ -124,19 +134,19 @@ export async function downloadIpfs(_e?: IpcMainInvokeEvent, { log = logger.log }
log(`Installing ipfs version ${latestVersionNumber}`);
}

const downloadUrl = `https://dist.ipfs.tech/go-ipfs/${latestVersion}/go-ipfs_${latestVersion}_darwin-${targetArch}.tar.gz`;
const downloadUrl = `https://dist.ipfs.tech/go-ipfs/${latestVersion}/go-ipfs_${latestVersion}_${osPlatform}-${targetArch}.${fileExt}`;
log(`IPFS package: ${downloadUrl}`);

await fs.rm(path.join(IPFS_PATH, 'config'), { recursive: true });
await fs.rm(path.join(IPFS_PATH, 'config'), { recursive: true }).catch(() => {});
await fs.mkdir(ROOT, { recursive: true });
await new Promise((resolve, reject) => {
const file = createWriteStream(path.join(ROOT, 'ipfs.tar.gz'));
const file = createWriteStream(path.join(ROOT, `ipfs.${fileExt}`));
https.get(downloadUrl, (response) => pipeline(response, file).then(resolve).catch(reject));
});

await new Promise((resolve, reject) => {
createReadStream(path.join(ROOT, 'ipfs.tar.gz'))
.pipe(zlib.createGunzip())
createReadStream(path.join(ROOT, `ipfs.${fileExt}`))
.pipe(fileExt === 'zip' ? unzipper.Extract({ path: ROOT }) : zlib.createGunzip())
.pipe(tar.extract({ cwd: ROOT }))
.on('error', reject)
.on('end', resolve);
Expand Down
23 changes: 13 additions & 10 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,19 @@ function updateContextMenu() {
])
);
}
app.dock.setMenu(
Menu.buildFromTemplate([
menu.app,
menu.autoStart,
menu.devTools,
menu.tray,
{ type: 'separator' },
...menu.dapps,
])
);

if (app.dock) {
app.dock.setMenu(
Menu.buildFromTemplate([
menu.app,
menu.autoStart,
menu.devTools,
menu.tray,
{ type: 'separator' },
...menu.dapps,
])
);
}
}

function createWindow() {
Expand Down
41 changes: 31 additions & 10 deletions src/main/pid.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { exec, execSync } from 'child_process';
import logger from 'electron-log';

export function extractPids(processes: string): number[] {
const isWin = process.platform === 'win32';
return processes
.trim()
.split('\n')
.filter((line) => !line.includes('grep'))
.map((line) => {
const [raw] = line.trim().split(' ');
return parseInt(raw, 10);
if (isWin) {
const parts = line.trim().split(' ').filter(Boolean);
return parseInt(parts[1], 10); // On Windows, PID is the second column
} else {
const [raw] = line.trim().split(' ');
return parseInt(raw, 10);
}
});
}

Expand All @@ -18,9 +25,14 @@ export function findPid(processes: string): number | undefined {

export function getPidSync(search: string): number | undefined {
try {
const processes = execSync(`ps -ax | grep "${search}"`, {
encoding: 'utf8',
});
const processes = execSync(
process.platform === 'win32'
? `tasklist /fi "IMAGENAME eq ${search}" /fo table /nh`
: `ps -ax | grep '${search}'`,
{
encoding: 'utf8',
}
);
return findPid(processes);
} catch (_e) {
// whatever
Expand All @@ -29,9 +41,14 @@ export function getPidSync(search: string): number | undefined {

export function getPidsSync(search: string): number[] {
try {
const processes = execSync(`ps -ax | grep '${search}'`, {
encoding: 'utf8',
});
const processes = execSync(
process.platform === 'win32'
? `tasklist /fi "IMAGENAME eq ${search}" /fo table /nh`
: `ps -ax | grep '${search}'`,
{
encoding: 'utf8',
}
);
return extractPids(processes);
} catch (_e) {
return [];
Expand All @@ -53,9 +70,13 @@ function execCommand(command: string): Promise<string> {

export async function getPid(search: string): Promise<number | undefined> {
try {
const processes = await execCommand(`ps -ax | grep '${search}'`);
const processes = await execCommand(
process.platform === 'win32'
? `tasklist /fi "IMAGENAME eq ${search}" /fo table /nh`
: `ps -ax | grep '${search}'`
);
return findPid(processes);
} catch (_e) {
// whatever
logger.error(_e);
}
}
9 changes: 9 additions & 0 deletions src/main/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { URL } from 'url';
import path from 'path';
import os from 'os';

export function resolveHtmlPath(htmlFileName: string) {
if (process.env.NODE_ENV === 'development') {
Expand All @@ -10,3 +11,11 @@ export function resolveHtmlPath(htmlFileName: string) {
}
return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`;
}

export function getPlatformDetails() {
const arch = os.arch();
const targetArch = arch === 'x64' ? 'amd64' : 'arm64';
const osPlatform = process.platform === 'darwin' ? 'darwin' : 'windows';
const fileExt = osPlatform === 'darwin' ? 'tar.gz' : 'zip';
return { osPlatform, fileExt, targetArch };
}

0 comments on commit 0eceae3

Please sign in to comment.