Files

265 lines
7.3 KiB
JavaScript

import * as childProcess from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const workspaceDir = path.resolve(packageDir, '../..');
const electronBin = path.join(workspaceDir, 'node_modules', '.bin', 'electron');
const electronEntry = path.join(packageDir, 'dist_ts', 'main.js');
const getUid = () => process.getuid?.() ?? 1000;
const isSocket = (filePath) => {
if (!filePath) {
return false;
}
try {
return fs.statSync(filePath).isSocket();
} catch {
return false;
}
};
const parseAgentEnv = () => {
const agentEnvPath = path.join(os.homedir(), '.ssh', 'agent.env');
try {
const agentEnvText = fs.readFileSync(agentEnvPath, 'utf8');
const socketMatch = agentEnvText.match(/(?:^|\n)SSH_AUTH_SOCK=([^;\n]+)/);
return socketMatch?.[1];
} catch {
return undefined;
}
};
const findTmpAgentSocket = () => {
let candidates = [];
try {
const tmpEntries = fs.readdirSync('/tmp', { withFileTypes: true });
for (const entry of tmpEntries) {
if (!entry.isDirectory() || !entry.name.startsWith('ssh-')) {
continue;
}
const directory = path.join('/tmp', entry.name);
try {
for (const socketEntry of fs.readdirSync(directory, { withFileTypes: true })) {
if (!socketEntry.name.startsWith('agent.')) {
continue;
}
const socketPath = path.join(directory, socketEntry.name);
if (isSocket(socketPath)) {
candidates.push({ path: socketPath, mtimeMs: fs.statSync(socketPath).mtimeMs });
}
}
} catch {}
}
} catch {}
candidates = candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
return candidates[0]?.path;
};
const resolveSshAuthSock = (env) => {
if (isSocket(env.SSH_AUTH_SOCK)) {
return env.SSH_AUTH_SOCK;
}
const agentEnvSocket = parseAgentEnv();
if (isSocket(agentEnvSocket)) {
return agentEnvSocket;
}
return findTmpAgentSocket();
};
const commandExists = (command) => {
try {
childProcess.execFileSync('which', [command], { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const isSwayRunning = () => {
try {
childProcess.execFileSync('pgrep', ['-x', 'sway'], { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
const getWaylandSocket = () => {
const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${getUid()}`;
try {
return fs.readdirSync(runtimeDir).find((file) => file.startsWith('wayland-'));
} catch {
return undefined;
}
};
const removeStaleWaylandSocket = (socket) => {
if (!socket || isSwayRunning()) {
return;
}
const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${getUid()}`;
for (const file of [socket, `${socket}.lock`]) {
try {
fs.unlinkSync(path.join(runtimeDir, file));
} catch {}
}
};
const detectLinuxDisplay = () => {
if (process.env.WAYLAND_DISPLAY) {
return { type: 'wayland', socket: process.env.WAYLAND_DISPLAY };
}
if (process.env.DISPLAY) {
return { type: 'x11' };
}
const existingSocket = getWaylandSocket();
if (existingSocket) {
if (isSwayRunning()) {
return { type: 'wayland', socket: existingSocket };
}
removeStaleWaylandSocket(existingSocket);
}
try {
const driDevices = fs.readdirSync('/dev/dri');
if (driDevices.some((device) => device.startsWith('card'))) {
return { type: 'drm' };
}
} catch {}
return { type: 'none' };
};
const startSway = async () => {
if (!commandExists('sway')) {
throw new Error('No display is available and sway is not installed.');
}
const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${getUid()}`;
const configDir = path.join(runtimeDir, 'gitzone-ide-sway');
const configPath = path.join(configDir, 'config');
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(
configPath,
['default_border none', 'default_floating_border none', 'hide_edge_borders both', ''].join('\n'),
);
console.log('Starting Sway for Git.Zone IDE...');
const swayProcess = childProcess.spawn('sway', ['--unsupported-gpu', '-c', configPath], {
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
XDG_RUNTIME_DIR: runtimeDir,
XDG_SESSION_TYPE: 'wayland',
LIBSEAT_BACKEND: process.env.LIBSEAT_BACKEND || 'seatd',
WLR_LIBINPUT_NO_DEVICES: '1',
},
});
let stderr = '';
swayProcess.stderr?.on('data', (chunk) => {
stderr += chunk.toString();
});
const startedAt = Date.now();
while (Date.now() - startedAt < 10000) {
const socket = getWaylandSocket();
if (socket) {
console.log(`Sway started on ${socket}.`);
return { process: swayProcess, socket };
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
try {
swayProcess.kill('SIGTERM');
} catch {}
throw new Error(`Sway did not start within 10s.${stderr ? `\n${stderr}` : ''}`);
};
const stopChildProcess = (child) => {
if (!child || child.killed) {
return;
}
try {
child.kill('SIGTERM');
} catch {}
};
const launchElectron = (args, env, swayProcess) => {
const electronProcess = childProcess.spawn(electronBin, args, {
cwd: packageDir,
stdio: 'inherit',
env,
});
const cleanup = () => stopChildProcess(swayProcess);
process.once('SIGINT', () => {
cleanup();
electronProcess.kill('SIGINT');
process.exit(0);
});
process.once('SIGTERM', () => {
cleanup();
electronProcess.kill('SIGTERM');
process.exit(0);
});
electronProcess.on('exit', (code, signal) => {
cleanup();
process.exit(code ?? (signal ? 1 : 0));
});
};
const main = async () => {
const electronArgs = [electronEntry, '--no-sandbox', ...process.argv.slice(2)];
const electronEnv = { ...process.env };
let swayProcess;
electronEnv.GITZONE_IDE_NODE_BINARY = process.execPath;
const sshAuthSock = resolveSshAuthSock(electronEnv);
if (sshAuthSock) {
electronEnv.SSH_AUTH_SOCK = sshAuthSock;
console.log(`Using SSH agent socket: ${sshAuthSock}`);
} else {
console.warn('No SSH agent socket found. System ssh will only use identity files and default keys.');
}
if (os.platform() === 'linux') {
const display = detectLinuxDisplay();
console.log(`Electron display mode: ${display.type}`);
if (display.type === 'wayland') {
electronEnv.WAYLAND_DISPLAY = display.socket;
electronEnv.XDG_RUNTIME_DIR = electronEnv.XDG_RUNTIME_DIR || `/run/user/${getUid()}`;
electronArgs.push('--ozone-platform=wayland');
} else if (display.type === 'drm') {
const sway = await startSway();
swayProcess = sway.process;
electronEnv.WAYLAND_DISPLAY = sway.socket;
electronEnv.XDG_RUNTIME_DIR = electronEnv.XDG_RUNTIME_DIR || `/run/user/${getUid()}`;
electronEnv.RUNTIME_ENV = 'sway';
electronArgs.push('--ozone-platform=wayland');
console.log('Git.Zone IDE is available on the local Sway display.');
} else if (display.type === 'none') {
throw new Error('No display is available. Start X11/Wayland, install sway, or run under xvfb-run.');
}
}
launchElectron(electronArgs, electronEnv, swayProcess);
};
main().catch((error) => {
console.error(error.stack || error.message || String(error));
process.exit(1);
});