265 lines
7.3 KiB
JavaScript
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);
|
|
});
|