import * as vscode from 'vscode';
import * as child_process from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { DaemonClient } from './client';
import { HealthResponse } from './types';
import { DEFAULT_PORT } from '../config';
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
interface DaemonBinary {
path: string;
args: string[];
}
export class DaemonProcess {
private process?: child_process.ChildProcess;
private client: DaemonClient;
private readonly extensionUri: vscode.Uri;
private readonly defaultPort: number;
private readonly configBinaryPath: string;
private readonly autoStart: boolean;
private _ensureRunningPromise: Promise<boolean> | null = null;
constructor(client: DaemonClient, extensionUri: vscode.Uri, opts?: { defaultPort?: number; binaryPath?: string; autoStart?: boolean }) {
this.client = client;
this.extensionUri = extensionUri;
this.defaultPort = opts?.defaultPort ?? DEFAULT_PORT;
this.configBinaryPath = opts?.binaryPath ?? '';
this.autoStart = opts?.autoStart ?? true;
}
async ensureRunning(): Promise<boolean> {
if (this._ensureRunningPromise) {
return this._ensureRunningPromise;
}
this._ensureRunningPromise = this._ensureRunningImpl().finally(() => {
this._ensureRunningPromise = null;
});
return this._ensureRunningPromise;
}
private async _ensureRunningImpl(): Promise<boolean> {
const health = await this.tryGetHealth();
if (health) {
const expected = this.getExpectedVersion();
if (!expected || health.version === expected) {
return true;
}
console.log(`[AtomCode] Daemon version mismatch: running=${health.version}, expected=${expected}. Restarting...`);
const shutdownOk = await this.shutdownDaemon();
if (shutdownOk) {
console.log('[AtomCode] Old daemon stopped successfully');
} else {
console.warn(
`[AtomCode] Refusing to start daemon because old daemon ${health.version} is still running; expected ${expected}`
);
vscode.window.showWarningMessage(
`AtomCode daemon version mismatch: running ${health.version}, expected ${expected}. AtomCode could not stop the old daemon. Please stop the old AtomCode daemon or reload VS Code.`
);
return false;
}
const started = await this.start();
if (!started) {
const postHealth = await this.tryGetHealth();
if (postHealth && postHealth.version === expected) {
console.log(`[AtomCode] Daemon restarted to version ${expected}`);
return true;
}
return false;
}
const newHealth = await this.tryGetHealth();
if (newHealth && newHealth.version === expected) {
console.log(`[AtomCode] Daemon restarted to version ${expected}`);
return true;
}
if (newHealth) {
console.warn(`[AtomCode] New daemon version ${newHealth.version} does not match expected ${expected}`);
}
return false;
}
if (!this.autoStart) {
return false;
}
return this.start();
}
private getExpectedVersion(): string {
const versionFile = path.join(this.extensionUri.fsPath, 'resources', 'bin', 'daemon-version.txt');
try {
const version = fs.readFileSync(versionFile, 'utf-8').trim();
if (version) {
return version;
}
} catch {
}
console.warn('[AtomCode] Could not read daemon-version.txt, skipping version check');
return '';
}
private async tryGetHealth(): Promise<HealthResponse | null> {
try {
return await this.client.health();
} catch {
return null;
}
}
private async shutdownDaemon(): Promise<boolean> {
try {
await this.client.shutdown();
} catch {
}
const deadline = Date.now() + 5000;
while (Date.now() < deadline) {
await sleep(100);
if (!(await this.client.isRunning())) {
return true;
}
}
if (this.process && !this.process.killed) {
console.warn('[AtomCode] Graceful shutdown timed out, sending SIGTERM to owned daemon process');
try {
this.process.kill('SIGTERM');
} catch {
}
const termDeadline = Date.now() + 2000;
while (Date.now() < termDeadline) {
await sleep(100);
if (!(await this.client.isRunning())) {
return true;
}
}
if (!this.process.killed) {
console.warn('[AtomCode] SIGTERM failed, sending SIGKILL to owned daemon process');
try {
this.process.kill('SIGKILL');
} catch { }
await sleep(300);
}
}
if (!(await this.client.isRunning())) {
return true;
}
console.warn('[AtomCode] Daemon did not exit. It may have been started by another process.');
return false;
}
private async start(): Promise<boolean> {
const port = this.defaultPort;
const binary = this.findBinary(port);
if (!binary) {
vscode.window.showErrorMessage(
'AtomCode daemon not found for this platform. Reinstall the AtomCode extension, install atomcode, or set atomcode.daemon.binaryPath in settings.'
);
return false;
}
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
this.process = child_process.spawn(binary.path, binary.args, {
detached: true,
stdio: 'ignore',
windowsHide: true,
...(cwd ? { cwd } : {}),
});
this.process.unref();
for (let i = 0; i < 100; i++) {
await new Promise((r) => setTimeout(r, 100));
if (await this.client.isRunning()) {
return true;
}
}
vscode.window.showWarningMessage(
`AtomCode daemon started but not responding. Check if port ${port} is available.`
);
return false;
}
* Find the atomcode binary. Returns the path and args to start the daemon.
*
* Search order:
* 1. User-configured binaryPath
* 2. Bundled standalone atomcode-daemon in the extension package
* 3. `atomcode` in PATH (uses `atomcode daemon` subcommand)
* 4. Common install locations
* 5. Workspace build outputs (for developers)
*/
private findBinary(port: number): DaemonBinary | undefined {
const portArgs = ['--port', String(port), '--client', 'vscode'];
if (this.configBinaryPath && fs.existsSync(this.configBinaryPath)) {
const name = path.basename(this.configBinaryPath);
if (name.includes('daemon')) {
return { path: this.configBinaryPath, args: portArgs };
}
return { path: this.configBinaryPath, args: ['daemon', ...portArgs] };
}
const bundled = this.findBundledDaemon();
if (bundled) {
return { path: bundled, args: portArgs };
}
try {
const command = process.platform === 'win32' ? 'where atomcode' : 'which atomcode 2>/dev/null';
const resolved = child_process.execSync(command, { encoding: 'utf-8' }).trim();
if (resolved) {
const firstMatch = process.platform === 'win32' ? resolved.split('\n')[0].trim() : resolved;
return { path: firstMatch, args: ['daemon', ...portArgs] };
}
} catch {
}
const home: string = os.homedir();
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
const atomcodePaths = [
path.join(home, '.atomcode', 'bin', 'atomcode'),
path.join(home, '.cargo', 'bin', 'atomcode'),
'/usr/local/bin/atomcode',
];
for (const p of atomcodePaths) {
if (fs.existsSync(p)) {
return { path: p, args: ['daemon', ...portArgs] };
}
}
const daemonPaths = [
path.join(home, '.atomcode', 'bin', 'atomcode-daemon'),
path.join(home, '.cargo', 'bin', 'atomcode-daemon'),
'/usr/local/bin/atomcode-daemon',
];
for (const p of daemonPaths) {
if (fs.existsSync(p)) {
return { path: p, args: portArgs };
}
}
const devPaths = [
path.join(workspaceRoot, 'target', 'release', 'atomcode-daemon'),
path.join(workspaceRoot, 'target', 'debug', 'atomcode-daemon'),
];
for (const p of devPaths) {
if (fs.existsSync(p)) {
console.warn(`[AtomCode] Using dev build daemon: ${p}. The bundled daemon was not found — the extension package may be missing resources/bin/<platform>/atomcode-daemon.`);
vscode.window.showWarningMessage(
`AtomCode is using a development build of the daemon (${p}). The bundled daemon was not found. Reinstall the extension or set atomcode.daemon.binaryPath in settings.`
);
return { path: p, args: portArgs };
}
}
return undefined;
}
private findBundledDaemon(): string | undefined {
const platformDir = this.platformDir();
if (!platformDir) {
return undefined;
}
const executable = process.platform === 'win32' ? 'atomcode-daemon.exe' : 'atomcode-daemon';
const bundled = path.join(this.extensionUri.fsPath, 'resources', 'bin', platformDir, executable);
if (!fs.existsSync(bundled)) {
return undefined;
}
this.ensureExecutable(bundled);
return bundled;
}
private platformDir(): string | undefined {
const platform = process.platform;
const arch = process.arch;
if (platform === 'darwin' && arch === 'arm64') return 'darwin-arm64';
if (platform === 'darwin' && arch === 'x64') return 'darwin-x64';
if (platform === 'linux' && arch === 'x64') return 'linux-x64';
if (platform === 'linux' && arch === 'arm64') return 'linux-arm64';
if (platform === 'win32' && arch === 'x64') return 'win32-x64';
return undefined;
}
private ensureExecutable(filePath: string): void {
if (process.platform === 'win32') {
return;
}
try {
fs.accessSync(filePath, fs.constants.X_OK);
} catch {
try {
fs.chmodSync(filePath, 0o755);
} catch {
}
}
}
dispose(): void {
this.process = undefined;
}
}