From 3fc0db410006be428ac87513fc2d7bae2ae9b56e Mon Sep 17 00:00:00 2001 From: "J. Courtland Colburn" Date: Thu, 5 Mar 2026 10:57:19 -0500 Subject: [PATCH 1/2] fix(doctor): detect virtiofsd installed outside PATH on Debian On Debian/Ubuntu, the virtiofsd package intentionally installs its binary to /usr/libexec/virtiofsd (with a symlink at /usr/lib/qemu/virtiofsd) rather than a PATH-accessible location like /usr/bin. This is by design: /usr/libexec/ is reserved for executables invoked by other programs, not end users, and the package's vhost-user registration file (/usr/share/qemu/vhost-user/50-virtiofsd.json) explicitly points QEMU to /usr/libexec/virtiofsd as the canonical binary location. The previous `command -v virtiofsd` check only searches PATH, producing a false [WARN] even when virtiofsd is properly installed. Likewise the runtime spawn used just 'virtiofsd', which would fail at launch time. Fix both call sites: - launcher-common.sh: replace the generic tool-loop entry with a dedicated check that tries PATH first, then probes the known non-PATH locations and reports the resolved path in the [PASS] message - cowork-vm-service.js: add findVirtiofsd() helper using the same probe order, and guard the virtiofsd spawn behind a path-found check Co-Authored-By: Claude Sonnet 4.6 --- scripts/cowork-vm-service.js | 56 ++++++++++++++++++++++++++---------- scripts/launcher-common.sh | 25 +++++++++++++++- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/scripts/cowork-vm-service.js b/scripts/cowork-vm-service.js index 74735d31..bc5ddb00 100644 --- a/scripts/cowork-vm-service.js +++ b/scripts/cowork-vm-service.js @@ -52,6 +52,26 @@ const LOG_FILE = path.join( process.env.HOME || '/tmp', '.config', 'Claude', 'logs', 'cowork_vm_daemon.log' ); +function findVirtiofsd() { + const candidates = [ + '/usr/libexec/virtiofsd', + '/usr/lib/qemu/virtiofsd', + ]; + try { + const result = execFileSync('which', ['virtiofsd'], { + encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + if (result) return result; + } catch (_) { /* not in PATH */ } + for (const p of candidates) { + try { + fs.accessSync(p, fs.constants.X_OK); + return p; + } catch (_) { /* not found */ } + } + return null; +} + function formatArgs(args) { return args.map(a => typeof a === 'string' ? a : JSON.stringify(a)) .join(' '); @@ -980,22 +1000,28 @@ class KvmBackend extends BackendBase { const initrdPath = path.join(VM_BASE_DIR, 'initrd'); // Start virtiofsd for home directory share (if available) - try { - const virtiofsSock = path.join(this.sessionDir, 'virtiofs.sock'); - this.virtiofsdProcess = spawnProcess('virtiofsd', [ - `--socket-path=${virtiofsSock}`, - '-o', `source=${os.homedir()}`, - '-o', 'cache=auto', - ], { - stdio: ['ignore', 'pipe', 'pipe'], - }); - this.virtiofsdProcess.on('error', (err) => { - log('KvmBackend: virtiofsd error:', err.message); + const virtiofsdBin = findVirtiofsd(); + if (virtiofsdBin) { + try { + const virtiofsSock = path.join(this.sessionDir, 'virtiofs.sock'); + this.virtiofsdProcess = spawnProcess(virtiofsdBin, [ + `--socket-path=${virtiofsSock}`, + '-o', `source=${os.homedir()}`, + '-o', 'cache=auto', + ], { + stdio: ['ignore', 'pipe', 'pipe'], + }); + this.virtiofsdProcess.on('error', (err) => { + log('KvmBackend: virtiofsd error:', err.message); + this.virtiofsdProcess = null; + }); + log(`KvmBackend: virtiofsd started, socket=${virtiofsSock}`); + } catch (e) { + log(`KvmBackend: virtiofsd not available: ${e.message}`); this.virtiofsdProcess = null; - }); - log(`KvmBackend: virtiofsd started, socket=${virtiofsSock}`); - } catch (e) { - log(`KvmBackend: virtiofsd not available: ${e.message}`); + } + } else { + log('KvmBackend: virtiofsd not found, skipping home directory share'); this.virtiofsdProcess = null; } diff --git a/scripts/launcher-common.sh b/scripts/launcher-common.sh index 20b49948..72278c0e 100755 --- a/scripts/launcher-common.sh +++ b/scripts/launcher-common.sh @@ -470,12 +470,35 @@ print(len(servers)) _info 'Fix: sudo modprobe vhost_vsock' fi + # virtiofsd — Debian installs it outside PATH (/usr/libexec or /usr/lib/qemu) + local _virtiofsd_bin='' + if command -v virtiofsd &>/dev/null; then + _virtiofsd_bin=$(command -v virtiofsd) + else + local _vfsd_candidate + for _vfsd_candidate in \ + /usr/libexec/virtiofsd \ + /usr/lib/qemu/virtiofsd + do + if [[ -x $_vfsd_candidate ]]; then + _virtiofsd_bin="$_vfsd_candidate" + break + fi + done + fi + + if [[ -n $_virtiofsd_bin ]]; then + _pass "virtiofsd: found ($_virtiofsd_bin)" + else + _warn 'virtiofsd: not found' + _info "Fix: $(_cowork_pkg_hint "$_distro_id" virtiofsd)" + fi + # Check required tools: label, binary, pkg-hint name local _tool_label _tool_bin _tool_pkg for _tool_label in \ 'QEMU:qemu-system-x86_64:qemu' \ 'socat:socat:socat' \ - 'virtiofsd:virtiofsd:virtiofsd' \ 'bubblewrap:bwrap:bubblewrap' do _tool_bin="${_tool_label#*:}" From c98a7660e28a474009342d088188894fa8080651 Mon Sep 17 00:00:00 2001 From: "J. Courtland Colburn" Date: Thu, 5 Mar 2026 11:02:00 -0500 Subject: [PATCH 2/2] style: add JSDoc and blank line separator to findVirtiofsd() Add missing blank line between LOG_FILE constant and findVirtiofsd() function, and add a JSDoc comment documenting the function's purpose and return value. Co-Authored-By: Claude --- scripts/cowork-vm-service.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/cowork-vm-service.js b/scripts/cowork-vm-service.js index bc5ddb00..34b8343b 100644 --- a/scripts/cowork-vm-service.js +++ b/scripts/cowork-vm-service.js @@ -52,6 +52,12 @@ const LOG_FILE = path.join( process.env.HOME || '/tmp', '.config', 'Claude', 'logs', 'cowork_vm_daemon.log' ); + +/** + * Locate virtiofsd binary. Debian installs it to /usr/libexec/virtiofsd which + * is outside PATH, so we check well-known locations after checking PATH. + * @returns {string|null} Absolute path to virtiofsd, or null if not found. + */ function findVirtiofsd() { const candidates = [ '/usr/libexec/virtiofsd',