Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
161
find-window.js
Normal file
161
find-window.js
Normal file
@@ -0,0 +1,161 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user