/** * Mount-point detection helpers for `dench-fs-daemon`. * * `/workspace` in every Dench sandbox is a Daytona persistent volume * mounted via AWS Mountpoint for S3 (a FUSE driver). Two upstream * limitations matter for the file daemon: * * • inotify events never fire on the mount * (https://github.com/awslabs/mountpoint-s3/issues/1290), so * chokidar's default backend is useless. The daemon switches * to polling mode when these helpers report a FUSE backing. * * • rename(2) returns ENOSYS on general-purpose buckets, so * callers that need to detect "is this path on the volume?" * can use `detectWorkspaceFsType` to choose the safe * copy+unlink path instead of trusting `mv`. * * This file lives alongside fs-daemon.ts but contains no Convex / * chokidar imports so unit tests can pull it directly. */ import { readFile } from "node:fs/promises"; export type MountInfo = { mountPoint: string; fsType: string; source: string; }; /** * Read /proc/self/mountinfo and return the parsed mount table. * Returns an empty array on non-Linux hosts (e.g. local macOS dev, * where /proc doesn't exist) so callers can fall through cleanly. * * The proc(5) line format is: * * 36 35 98:0 /mnt1 /mnt/parent rw,noatime master:1 - ext3 /dev/root rw * ─┬ ─┬ ──┬─ ──┬── ──┬─────── ──┬───── ──┬────── ─ ──┬─ ────┬──── ──┬ * 1 2 3 4 5 6 7 8 9 10 * * We only need fields 5 (mount point) and the fs type after the * " - " separator (field 9, with optional source as field 10). */ export async function readMountInfo( procPath = "/proc/self/mountinfo", ): Promise { let raw: string; try { raw = await readFile(procPath, "utf-8"); } catch { return []; } const out: MountInfo[] = []; for (const rawLine of raw.split("\n")) { const line = rawLine.trim(); if (!line) continue; const dashIdx = line.indexOf(" - "); if (dashIdx === -1) continue; const before = line.slice(0, dashIdx).split(/\s+/); const after = line.slice(dashIdx + 3).split(/\s+/); const mountPoint = before[4]; const fsType = after[0]; const source = after[1] ?? ""; if (!mountPoint || !fsType) continue; out.push({ mountPoint, fsType, source }); } return out; } /** * Find the longest mount point that is a prefix of `absPath`. Used to * answer "what filesystem is this path on?" from the parsed mount * table. Important to use longest-prefix so a `/workspace` mount * over `/` doesn't get shadowed by the root mount. * * Naïve `startsWith` would also match `/workspace` against * `/worker`; we guard against that by requiring the next char to be * `/` (or the strings to be equal). */ export function findMountForPath( mounts: MountInfo[], absPath: string, ): MountInfo | null { let best: MountInfo | null = null; for (const mount of mounts) { if (mount.mountPoint === absPath) return mount; const trimmed = mount.mountPoint.replace(/\/+$/, ""); if ( absPath === trimmed || absPath.startsWith(`${trimmed === "" ? "" : trimmed}/`) ) { if (!best || mount.mountPoint.length > best.mountPoint.length) { best = mount; } } } return best; } /** * Returns true for any FUSE-flavored fs type. mount-s3 reports * `fuse` (no subtype on Linux); other FUSE drivers commonly use * `fuse.` (e.g. fuse.sshfs). `fuseblk` is the block-device * flavor used by ntfs-3g and friends. */ export function isFuseFsType(fsType: string): boolean { if (!fsType) return false; return ( fsType === "fuse" || fsType.startsWith("fuse.") || fsType === "fuseblk" ); } /** * Convenience: detect the filesystem backing a given workspace path * in one call. The daemon uses this on startup to decide whether to * enable chokidar's polling mode. */ export async function detectWorkspaceFsType(workspace: string): Promise<{ fsType: string; isFuse: boolean; source: string; }> { const mounts = await readMountInfo(); const match = findMountForPath(mounts, workspace); if (!match) { return { fsType: "unknown", isFuse: false, source: "" }; } return { fsType: match.fsType, isFuse: isFuseFsType(match.fsType), source: match.source, }; }