162 lines
4.0 KiB
JavaScript
162 lines
4.0 KiB
JavaScript
import { readFileSync, readdirSync, readlinkSync } from 'fs';
|
|
import { execFileSync } from 'child_process';
|
|
|
|
function read_file(path) {
|
|
try {
|
|
return readFileSync(path, 'utf8');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function get_exe(pid) {
|
|
try {
|
|
return readlinkSync(`/proc/${pid}/exe`);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function get_cmdline(pid) {
|
|
const raw = read_file(`/proc/${pid}/cmdline`);
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
return raw.replace(/\0/g, ' ').trim();
|
|
}
|
|
|
|
function get_ppid(pid) {
|
|
const status = read_file(`/proc/${pid}/status`);
|
|
if (!status) {
|
|
return null;
|
|
}
|
|
const match = status.match(/^PPid:\s+(\d+)/m);
|
|
return match ? parseInt(match[1]) : null;
|
|
}
|
|
|
|
//const DOCKER_COMM_PATTERN = /^docker-compose$/;
|
|
const CLAUDE_CMDLINE_SIGNATURE = 'claude --dangerously-skip-permissions';
|
|
const DOCKER_COMM_PATTERN = /^docker-compose$/;
|
|
|
|
function get_comm(pid) {
|
|
return read_file(`/proc/${pid}/comm`)?.trim() ?? null;
|
|
}
|
|
|
|
export function list_docker_pids() {
|
|
const results = [];
|
|
for (const entry of readdirSync('/proc')) {
|
|
if (!/^\d+$/.test(entry)) {
|
|
continue;
|
|
}
|
|
const comm = get_comm(entry);
|
|
if (!comm || !DOCKER_COMM_PATTERN.test(comm)) {
|
|
continue;
|
|
}
|
|
const cmdline = get_cmdline(entry);
|
|
if (cmdline && cmdline.includes(CLAUDE_CMDLINE_SIGNATURE)) {
|
|
results.push({ pid: entry, comm, cmdline });
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
export function walk_to_konsole(pid) {
|
|
let current = String(pid);
|
|
const visited = new Set();
|
|
const chain = [];
|
|
while (current && current !== '0' && !visited.has(current)) {
|
|
visited.add(current);
|
|
const exe = get_exe(current);
|
|
chain.push({ pid: current, exe });
|
|
if (exe === '/usr/bin/konsole') {
|
|
return { konsole_pid: current, chain };
|
|
}
|
|
current = String(get_ppid(current));
|
|
}
|
|
return { konsole_pid: null, chain };
|
|
}
|
|
|
|
function get_net_client_list() {
|
|
try {
|
|
const out = execFileSync('xprop', ['-root', '_NET_CLIENT_LIST'], { encoding: 'utf8' });
|
|
// Format: _NET_CLIENT_LIST(WINDOW): window id # 0x..., 0x..., ...
|
|
const match = out.match(/window id # (.+)/);
|
|
if (!match) {
|
|
return [];
|
|
}
|
|
return match[1].split(',').map(s => parseInt(s.trim(), 16)).filter(Boolean);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function get_window_pid(window_id) {
|
|
try {
|
|
const hex = '0x' + window_id.toString(16);
|
|
const out = execFileSync('xprop', ['-id', hex, '_NET_WM_PID'], { encoding: 'utf8' });
|
|
const match = out.match(/= (\d+)/);
|
|
return match ? parseInt(match[1]) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function window_id_for_pid(pid) {
|
|
const target_pid = parseInt(pid);
|
|
for (const wid of get_net_client_list()) {
|
|
if (get_window_pid(wid) === target_pid) {
|
|
return '0x' + wid.toString(16);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function find_claude_window_by_process_tree({ debug = false } = {}) {
|
|
const docker_pids = list_docker_pids();
|
|
if (debug) {
|
|
console.error(`[find-window] docker pids (${docker_pids.length}):`);
|
|
for (const { pid, comm, cmdline } of docker_pids) {
|
|
console.error(` pid=${pid} comm=${comm} cmdline=${cmdline}`);
|
|
}
|
|
}
|
|
|
|
for (const { pid } of docker_pids) {
|
|
const { konsole_pid, chain } = walk_to_konsole(pid);
|
|
if (debug) {
|
|
const chain_str = chain.map(e => `${e.pid}(${e.exe?.split('/').pop() ?? '?'})`).join(' → ');
|
|
console.error(`[find-window] pid=${pid} chain: ${chain_str}`);
|
|
}
|
|
if (!konsole_pid) {
|
|
continue;
|
|
}
|
|
if (debug) {
|
|
console.error(`[find-window] found konsole pid=${konsole_pid}, looking up window...`);
|
|
}
|
|
const window_id = window_id_for_pid(konsole_pid);
|
|
if (debug) {
|
|
console.error(`[find-window] window id: ${window_id ?? 'not found'}`);
|
|
}
|
|
if (window_id) {
|
|
return window_id;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function find_claude_window_by_title(title = 'claude-docker') {
|
|
try {
|
|
const result = execFileSync(
|
|
'xdotool', ['search', '--name', title],
|
|
{ encoding: 'utf8' }
|
|
);
|
|
const ids = result.trim().split('\n').filter(Boolean);
|
|
return ids[0] ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function find_claude_window({ debug = false } = {}) {
|
|
return find_claude_window_by_process_tree({ debug }) ?? find_claude_window_by_title();
|
|
}
|