fix: harden native workspace root and docker gates

This commit is contained in:
2026-08-11 14:11:42 +02:00
parent ee87a59e1f
commit a5916f6177
16 changed files with 401 additions and 96 deletions
+9 -1
View File
@@ -1 +1,9 @@
{ "targets": [{ "target_name": "workspace_fs_at", "sources": ["workspace_fs_at.cc"], "cflags_cc": ["-std=c++17"], "defines": ["NAPI_VERSION=8"] }] }
{
"targets": [{
"target_name": "workspace_fs_at",
"sources": ["workspace_fs_at.cc"],
"cflags_cc": ["-std=c++17"],
"defines": ["NAPI_VERSION=8"],
"conditions": [["OS=='linux' or OS=='mac'", {}], ["OS=='win'", {"type": "none"}]]
}]
}
+101 -17
View File
@@ -7,24 +7,108 @@
#include <string>
#include <stdint.h>
#include <unordered_set>
#include <algorithm>
#ifdef __APPLE__
#include <sys/types.h>
#ifndef F_FULLFSYNC
#define F_FULLFSYNC 51
#endif
struct Handle { uint64_t magic; int fd; bool directory; bool closed; };
#endif
namespace {
static const uint64_t MAGIC=0x5448545746534154ULL;
static std::unordered_set<void*> handles;
static void finalize(napi_env env, void* data, void*) { Handle* h=(Handle*)data; if(h && handles.find(h)!=handles.end() && h->magic==MAGIC && !h->closed){::close(h->fd);h->closed=true;} handles.erase(h); delete h; }
static bool getHandle(napi_env env,napi_value v,Handle** out){ void* p=nullptr; if(napi_get_value_external(env,v,&p)!=napi_ok||!p||handles.find(p)==handles.end())return false; auto*h=(Handle*)p; if(h->magic!=MAGIC||h->closed)return false;*out=h;return true; }
static const char* errnoName(int e){switch(e){case EINVAL:return "EINVAL";case EBADF:return "EBADF";case EEXIST:return "EEXIST";case ENOENT:return "ENOENT";case ENOTDIR:return "ENOTDIR";case ELOOP:return "ELOOP";case EACCES:return "EACCES";case EPERM:return "EPERM";case EAGAIN:return "EAGAIN";default:return "EIO";}}
static napi_value error(napi_env env,const char* syscall,int e){ napi_value msg,err,code,sys; napi_create_string_utf8(env,strerror(e),NAPI_AUTO_LENGTH,&msg); napi_create_error(env,nullptr,msg,&err); napi_create_string_utf8(env,syscall,NAPI_AUTO_LENGTH,&sys); napi_set_named_property(env,err,"syscall",sys); napi_create_string_utf8(env,errnoName(e),NAPI_AUTO_LENGTH,&code); napi_set_named_property(env,err,"code",code); napi_value en; napi_create_int32(env,e,&en); napi_set_named_property(env,err,"errno",en); return err; }
static napi_value fail(napi_env env,const char*s,int e){ napi_value x=error(env,s,e); napi_throw(env,x); return nullptr; }
static bool str(napi_env env,napi_value v,std::string& out){ size_t n; if(napi_get_value_string_utf8(env,v,nullptr,0,&n)!=napi_ok)return false; out.resize(n); napi_get_value_string_utf8(env,v,out.data(),n+1,&n);return true; }
static napi_value statObj(napi_env env,const struct stat& st){ napi_value o,n; napi_create_object(env,&o); napi_create_bigint_uint64(env,(uint64_t)st.st_dev,&n); napi_set_named_property(env,o,"device",n); napi_create_bigint_uint64(env,(uint64_t)st.st_ino,&n); napi_set_named_property(env,o,"inode",n); napi_create_uint32(env,(uint32_t)st.st_mode,&n); napi_set_named_property(env,o,"mode",n); napi_create_uint32(env,(uint32_t)st.st_uid,&n); napi_set_named_property(env,o,"uid",n); napi_create_uint32(env,(uint32_t)st.st_gid,&n); napi_set_named_property(env,o,"gid",n); napi_create_bigint_uint64(env,(uint64_t)st.st_nlink,&n); napi_set_named_property(env,o,"nlink",n); return o; }
static napi_value result(napi_env env,int fd,bool dir,const struct stat&st){ auto*h=new Handle{MAGIC,fd,dir,false}; handles.insert(h); napi_value e,o,s; napi_create_external(env,h,finalize,nullptr,&e); napi_create_object(env,&o); napi_set_named_property(env,o,"handle",e); s=statObj(env,st); napi_set_named_property(env,o,"openedStat",s); return o; }
static napi_value openatFn(napi_env env,napi_callback_info info){ size_t argc=1; napi_value a[1]; if(napi_get_cb_info(env,info,&argc,a,nullptr,nullptr)!=napi_ok||argc!=1)return fail(env,"openat",EINVAL); napi_value pv,nv,kv,mv; if(napi_get_named_property(env,a[0],"parent",&pv)!=napi_ok||napi_get_named_property(env,a[0],"name",&nv)!=napi_ok||napi_get_named_property(env,a[0],"kind",&kv)!=napi_ok||napi_get_named_property(env,a[0],"createMode",&mv)!=napi_ok)return fail(env,"openat",EINVAL); std::string name,kind; if(!str(env,nv,name)||!str(env,kv,kind))return fail(env,"openat",EINVAL); int32_t mode; if(napi_get_value_int32(env,mv,&mode)!=napi_ok)return fail(env,"openat",EINVAL); bool root=name=="/"; if(!root&&(name.empty()||name.size()>255||name=="."||name==".."||name.find('/')!=std::string::npos||name.find('\0')!=std::string::npos))return fail(env,"openat",EINVAL); Handle*ph=nullptr; if(root){if(kind!="directory"||mode!=0)return fail(env,"openat",EINVAL);} else if(!getHandle(env,pv,&ph)||!ph->directory)return fail(env,"openat",EBADF); int fd; if(root)fd=::open("/",O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); else if(kind=="directory"&&mode==0)fd=::openat(ph->fd,name.c_str(),O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); else if(kind=="regular_lock"&&mode==0600)fd=::openat(ph->fd,name.c_str(),O_RDWR|O_CREAT|O_CLOEXEC|O_NOFOLLOW,0600); else return fail(env,"openat",EINVAL); if(fd<0)return fail(env,"openat",errno); struct stat st; if(::fstat(fd,&st)<0){int e=errno;::close(fd);return fail(env,"fstat",e);} if(kind=="directory"&&!S_ISDIR(st.st_mode)){::close(fd);return fail(env,"openat",ENOTDIR);} if(kind=="regular_lock"&&(!S_ISREG(st.st_mode)||(st.st_mode&0777)!=0600)){::close(fd);return fail(env,"openat",EPERM);} return result(env,fd,kind=="directory",st); }
static napi_value mkdiratFn(napi_env env,napi_callback_info info){size_t n=3; napi_value a[3]; napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;std::string s;int32_t m;if(n!=3||!getHandle(env,a[0],&h)||!h->directory||!str(env,a[1],s)||napi_get_value_int32(env,a[2],&m)!=napi_ok||m!=0700)return fail(env,"mkdirat",EINVAL);if(s.empty()||s.size()>255||s=="."||s==".."||s.find('/')!=std::string::npos||s.find('\0')!=std::string::npos)return fail(env,"mkdirat",EINVAL);if(::mkdirat(h->fd,s.c_str(),0700)<0)return fail(env,"mkdirat",errno);return nullptr;}
static napi_value fstatatFn(napi_env env,napi_callback_info info){size_t n=2;napi_value a[2];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;std::string s;if(n!=2||!getHandle(env,a[0],&h)||!h->directory||!str(env,a[1],s)||s.empty()||s.size()>255||s=="."||s==".."||s.find('/')!=std::string::npos||s.find('\0')!=std::string::npos)return fail(env,"fstatat",EINVAL);struct stat st;if(::fstatat(h->fd,s.c_str(),&st,AT_SYMLINK_NOFOLLOW)<0)return fail(env,"fstatat",errno);return statObj(env,st);}
static napi_value fsyncFn(napi_env env,napi_callback_info info){size_t n=1;napi_value a[1];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;if(n!=1||!getHandle(env,a[0],&h)||!h->directory)return fail(env,"fsync",EBADF);if(::fsync(h->fd)<0)return fail(env,"fsync",errno);return nullptr;}
static napi_value closeFn(napi_env env,napi_callback_info info){size_t n=1;napi_value a[1];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;if(n!=1||!getHandle(env,a[0],&h))return fail(env,"close",EBADF);h->closed=true;int rc=::close(h->fd);if(rc<0)return fail(env,"close",errno);return nullptr;}
static napi_value fdFn(napi_env env,napi_callback_info info){size_t n=1;napi_value a[1];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;if(n!=1||!getHandle(env,a[0],&h))return fail(env,"fcntl",EBADF);napi_value x;napi_create_int32(env,h->fd,&x);return x;}
static napi_value init(napi_env env,napi_value exports){napi_property_descriptor p[]={{"openat",0,openatFn,0,0,0,napi_enumerable,0},{"mkdirat",0,mkdiratFn,0,0,0,napi_enumerable,0},{"fstatat",0,fstatatFn,0,0,0,napi_enumerable,0},{"fsyncDirectory",0,fsyncFn,0,0,0,napi_enumerable,0},{"close",0,closeFn,0,0,0,napi_enumerable,0},{"fdNumberForSynchronousBorrow",0,fdFn,0,0,0,napi_enumerable,0}};napi_define_properties(env,exports,6,p);return exports;} NAPI_MODULE(NODE_GYP_MODULE_NAME,init)
static const char OWNER_TOKEN = 0;
struct Handle { uint64_t magic; const void* owner; int fd; bool directory; bool closed; unsigned borrows; };
static std::unordered_set<Handle*> handles;
static void finalize(napi_env, void* data, void*) {
auto *h=static_cast<Handle*>(data);
if (!h) return;
if (handles.erase(h) && !h->closed) { h->closed=true; int rc = ::close(h->fd); (void)rc; }
delete h;
}
static bool getHandle(napi_env env,napi_value v,Handle** out) {
void *p=nullptr;
if (napi_get_value_external(env,v,&p)!=napi_ok || !p) return false;
auto *h=static_cast<Handle*>(p);
if (handles.find(h)==handles.end() || h->magic!=MAGIC || h->owner!=&OWNER_TOKEN || h->closed) return false;
*out=h; return true;
}
static const char* errnoName(int e) {
switch(e) {
case EINVAL:return "EINVAL"; case EBADF:return "EBADF"; case EEXIST:return "EEXIST";
case ENOENT:return "ENOENT"; case ENOTDIR:return "ENOTDIR"; case ELOOP:return "ELOOP";
case EACCES:return "EACCES"; case EPERM:return "EPERM"; case EAGAIN:return "EAGAIN";
case EBUSY:return "EBUSY"; case ENOSPC:return "ENOSPC"; case EDQUOT:return "EDQUOT";
case ENAMETOOLONG:return "ENAMETOOLONG"; case EROFS:return "EROFS"; case EISDIR:return "EISDIR";
case ENFILE:return "ENFILE"; case EMFILE:return "EMFILE"; case ENOMEM:return "ENOMEM";
case EIO:return "EIO"; case EINTR:return "EINTR"; case ENXIO:return "ENXIO";
default: return nullptr;
}
}
static napi_value error(napi_env env,const char* syscall,int e,const char* forced=nullptr) {
napi_value msg,err,code,sys;
const char *name=forced?forced:errnoName(e);
std::string fallback;
if (!name) { fallback="ERR_WORKSPACE_FS_AT_ERRNO_"+std::to_string(e); name=fallback.c_str(); }
napi_create_string_utf8(env,forced?forced:strerror(e),NAPI_AUTO_LENGTH,&msg);
napi_create_error(env,nullptr,msg,&err);
napi_create_string_utf8(env,syscall,NAPI_AUTO_LENGTH,&sys); napi_set_named_property(env,err,"syscall",sys);
napi_create_string_utf8(env,name,NAPI_AUTO_LENGTH,&code); napi_set_named_property(env,err,"code",code);
napi_value en; napi_create_int32(env,e,&en); napi_set_named_property(env,err,"errno",en);
return err;
}
static napi_value fail(napi_env env,const char*s,int e,const char* forced=nullptr){ napi_value x=error(env,s,e,forced); napi_throw(env,x); return nullptr; }
static bool str(napi_env env,napi_value v,std::string& out) {
size_t n=0; if(napi_get_value_string_utf8(env,v,nullptr,0,&n)!=napi_ok) return false;
out.resize(n); size_t got=0; if(napi_get_value_string_utf8(env,v,out.data(),n+1,&got)!=napi_ok) return false; out.resize(got); return true;
}
static bool validComponent(const std::string& s) {
return !s.empty() && s.size()<=255 && s!="." && s!=".." && s.find('/')==std::string::npos && s.find('\0')==std::string::npos;
}
static int openRetry(int dir,const char *name,int flags,mode_t mode) { int fd; do {fd=dir<0 ? ::open(name,flags,mode) : ::openat(dir,name,flags,mode);} while(fd<0&&errno==EINTR); return fd; }
static int statRetry(int fd, struct stat *st) { int rc; do {rc=::fstat(fd,st);} while(rc<0&&errno==EINTR); return rc; }
static napi_value statObj(napi_env env,const struct stat& st) {
napi_value o,n; napi_create_object(env,&o);
napi_create_bigint_uint64(env,(uint64_t)st.st_dev,&n); napi_set_named_property(env,o,"device",n);
napi_create_bigint_uint64(env,(uint64_t)st.st_ino,&n); napi_set_named_property(env,o,"inode",n);
napi_create_uint32(env,(uint32_t)st.st_mode,&n); napi_set_named_property(env,o,"mode",n);
napi_create_uint32(env,(uint32_t)st.st_uid,&n); napi_set_named_property(env,o,"uid",n);
napi_create_uint32(env,(uint32_t)st.st_gid,&n); napi_set_named_property(env,o,"gid",n);
napi_create_bigint_uint64(env,(uint64_t)st.st_nlink,&n); napi_set_named_property(env,o,"nlink",n); return o;
}
static napi_value result(napi_env env,int fd,bool dir,const struct stat&st) {
auto*h=new Handle{MAGIC,&OWNER_TOKEN,fd,dir,false,0}; handles.insert(h);
napi_value e,o,s; napi_create_external(env,h,finalize,nullptr,&e); napi_create_object(env,&o);
napi_set_named_property(env,o,"handle",e); s=statObj(env,st); napi_set_named_property(env,o,"openedStat",s); return o;
}
static bool getInput(napi_env env,napi_callback_info info,napi_value *a,size_t *argc) { return napi_get_cb_info(env,info,argc,a,nullptr,nullptr)==napi_ok; }
static napi_value openatFn(napi_env env,napi_callback_info info) {
size_t argc=1; napi_value a[1]; if(!getInput(env,info,a,&argc)||argc!=1)return fail(env,"openat",EINVAL);
napi_value pv,nv,kv,mv; if(napi_get_named_property(env,a[0],"parent",&pv)!=napi_ok||napi_get_named_property(env,a[0],"name",&nv)!=napi_ok||napi_get_named_property(env,a[0],"kind",&kv)!=napi_ok||napi_get_named_property(env,a[0],"createMode",&mv)!=napi_ok)return fail(env,"openat",EINVAL);
std::string name,kind; if(!str(env,nv,name)||!str(env,kv,kind))return fail(env,"openat",EINVAL); int32_t mode;if(napi_get_value_int32(env,mv,&mode)!=napi_ok)return fail(env,"openat",EINVAL);
bool root=name=="/"; if(!root&&!validComponent(name))return fail(env,"openat",EINVAL); Handle*ph=nullptr;
if(root){if(kind!="directory"||mode!=0)return fail(env,"openat",EINVAL);} else if(!getHandle(env,pv,&ph)||!ph->directory)return fail(env,"openat",EBADF);
int fd=-1;
if(root) fd=openRetry(-1,"/",O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW,0);
else if(kind=="directory"&&mode==0) { fd=openRetry(ph->fd,name.c_str(),O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW,0); }
else if(kind=="regular_lock"&&mode==0600) fd=openRetry(ph->fd,name.c_str(),O_RDWR|O_CREAT|O_CLOEXEC|O_NOFOLLOW,0600);
else return fail(env,"openat",EINVAL);
if(fd<0)return fail(env,"openat",errno);
struct stat st; if(statRetry(fd,&st)<0){int e=errno;::close(fd);return fail(env,"fstat",e);}
if(kind=="directory"&&!S_ISDIR(st.st_mode)){::close(fd);return fail(env,"openat",ENOTDIR);}
if(kind=="regular_lock" && (!S_ISREG(st.st_mode)||(st.st_mode&0777)!=0600||st.st_uid!=(uid_t)::geteuid()||st.st_nlink!=1)) {::close(fd);return fail(env,"openat",EPERM);}
return result(env,fd,kind=="directory",st);
}
static napi_value mkdiratFn(napi_env env,napi_callback_info info){size_t n=3;napi_value a[3];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;std::string s;int32_t m;if(n!=3||!getHandle(env,a[0],&h)||!h->directory||!str(env,a[1],s)||napi_get_value_int32(env,a[2],&m)!=napi_ok||m!=0700||!validComponent(s))return fail(env,"mkdirat",EINVAL);int rc;do{rc=::mkdirat(h->fd,s.c_str(),0700);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"mkdirat",errno);return nullptr;}
static napi_value fstatatFn(napi_env env,napi_callback_info info){size_t n=2;napi_value a[2];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;std::string s;if(n!=2||!getHandle(env,a[0],&h)||!h->directory||!str(env,a[1],s)||!validComponent(s))return fail(env,"fstatat",EINVAL);struct stat st;int rc;do{rc=::fstatat(h->fd,s.c_str(),&st,AT_SYMLINK_NOFOLLOW);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"fstatat",errno);return statObj(env,st);}
static napi_value fsyncFn(napi_env env,napi_callback_info info){size_t n=1;napi_value a[1];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;if(n!=1||!getHandle(env,a[0],&h)||!h->directory)return fail(env,"fsync",EBADF);int rc;do{rc=::fsync(h->fd);}while(rc<0&&errno==EINTR);
#ifdef __APPLE__
if(rc<0&&(errno==EINVAL||errno==ENOTSUP)){int frc;do{frc=::fcntl(h->fd,F_FULLFSYNC);}while(frc<0&&errno==EINTR);if(frc==0)return nullptr;rc=frc;}
#endif
if(rc<0)return fail(env,"fsync",errno);return nullptr;}
static napi_value closeFn(napi_env env,napi_callback_info info){size_t n=1;napi_value a[1];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h;if(n!=1||!getHandle(env,a[0],&h))return fail(env,"close",EBADF);if(h->borrows)return fail(env,"close",EBUSY);h->closed=true;handles.erase(h);int rc=::close(h->fd);if(rc<0){int e=errno;if(e==EINTR)return fail(env,"close",e,"ERR_WORKSPACE_FS_AT_CLOSE_UNCERTAIN");return fail(env,"close",e);}return nullptr;}
static napi_value withFdFn(napi_env env,napi_callback_info info){size_t n=2;napi_value a[2];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*h; napi_valuetype t;if(n!=2||!getHandle(env,a[0],&h)||napi_typeof(env,a[1],&t)!=napi_ok||t!=napi_function)return fail(env,"borrow",EINVAL);h->borrows++;napi_value argv; napi_create_int32(env,h->fd,&argv); napi_value out; napi_value global; napi_get_global(env, &global); napi_status rc=napi_call_function(env, global,a[1],1,&argv,&out);h->borrows--;if(rc!=napi_ok)return nullptr;return out;}
static napi_value duplicateForChildStdioFn(napi_env env,napi_callback_info info){size_t n=4;napi_value a[4];napi_get_cb_info(env,info,&n,a,nullptr,nullptr);Handle*w,*r;int32_t wf,rf;if(n!=4||!getHandle(env,a[0],&w)||!getHandle(env,a[1],&r)||w->directory||!r->directory||napi_get_value_int32(env,a[2],&wf)!=napi_ok||napi_get_value_int32(env,a[3],&rf)!=napi_ok||wf<0||rf<0)return fail(env,"dup2",EINVAL);if(w->borrows||r->borrows)return fail(env,"dup2",EBUSY);int rc;do{rc=::dup2(w->fd,wf);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"dup2",errno);do{rc=::dup2(r->fd,rf);}while(rc<0&&errno==EINTR);if(rc<0)return fail(env,"dup2",errno);return nullptr;}
static napi_value init(napi_env env,napi_value exports){napi_property_descriptor pub[]={{"openat",0,openatFn,0,0,0,napi_enumerable,0},{"mkdirat",0,mkdiratFn,0,0,0,napi_enumerable,0},{"fstatat",0,fstatatFn,0,0,0,napi_enumerable,0},{"fsyncDirectory",0,fsyncFn,0,0,0,napi_enumerable,0},{"close",0,closeFn,0,0,0,napi_enumerable,0}};napi_define_properties(env,exports,5,pub);napi_property_descriptor priv[]={{"withFd",0,withFdFn,0,0,0,napi_default,0},{"duplicateForChildStdio",0,duplicateForChildStdioFn,0,0,0,napi_default,0}};napi_define_properties(env,exports,2,priv);return exports;}
}
NAPI_MODULE(NODE_GYP_MODULE_NAME,init)
+3 -2
View File
@@ -5,11 +5,12 @@
"scripts": {
"dev": "tsx watch src/server.ts",
"prebuild": "node scripts/clean-dist.mjs",
"build": "tsc -p tsconfig.json",
"build": "npm run build:ts",
"test": "vitest run",
"start": "node dist/server.js",
"build:native": "node scripts/build-workspace-fs-at.mjs",
"postinstall": "npm run build:native"
"postinstall": "npm run build:native",
"build:ts": "node scripts/clean-dist.mjs && tsc -p tsconfig.json"
},
"dependencies": {
"@fastify/cors": "^11.2.0",
+9 -1
View File
@@ -1,8 +1,16 @@
import { existsSync, readdirSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const major = Number(process.versions.node.split(".")[0]);
if (major !== 22 || Number(process.versions.napi ?? 0) < 8) throw new Error(`workspace-fs-at requires Node 22 / N-API 8 (got ${process.versions.node} / N-API ${process.versions.napi})`);
if (process.platform !== "linux" && process.platform !== "darwin") throw new Error(`workspace-fs-at unsupported platform: ${process.platform}`);
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../native/workspace-fs-at");
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const result = spawnSync(npm, ["exec", "--", "node-gyp@11.2.0", "rebuild", "--offline"], { cwd: root, stdio: "inherit" });
const result = spawnSync(npm, ["exec", "--offline", "--", "node-gyp@11.2.0", "rebuild", "--offline"], { cwd: root, stdio: "inherit" });
if (result.error) throw result.error;
if (result.status !== 0) process.exit(result.status ?? 1);
const output = resolve(root, "build/Release/workspace_fs_at.node");
if (!existsSync(output)) throw new Error(`native addon output missing: ${output}`);
const outputs = readdirSync(resolve(root, "build/Release")).filter(name => name.endsWith(".node"));
if (outputs.length !== 1 || outputs[0] !== "workspace_fs_at.node") throw new Error(`unexpected native outputs: ${outputs.join(",")}`);
+8 -2
View File
@@ -3,6 +3,12 @@ declare const nativeWorkspaceFsAtComponentBrand: unique symbol;
export interface NativeWorkspaceFsAtHandleV1 { readonly [nativeWorkspaceFsAtHandleBrand]: true; }
export type NativeWorkspaceFsAtComponentV1 = string & { readonly [nativeWorkspaceFsAtComponentBrand]: true };
export interface NativeWorkspaceFsAtStatV1 { readonly device: bigint; readonly inode: bigint; readonly mode: number; readonly uid: number; readonly gid: number; readonly nlink: bigint; }
export interface NativeWorkspaceFsAtErrorV1 extends Error { readonly code:string; readonly errno:number; readonly syscall:"openat"|"mkdirat"|"fstat"|"fstatat"|"fsync"|"fcntl"|"close"; }
export interface NativeWorkspaceFsAtErrorV1 extends Error { readonly code:string; readonly errno:number; readonly syscall:"openat"|"mkdirat"|"fstat"|"fstatat"|"fsync"|"borrow"|"dup2"|"close"; }
export interface NativeWorkspaceFsAtOpenResultV1 { readonly handle: NativeWorkspaceFsAtHandleV1; readonly openedStat: NativeWorkspaceFsAtStatV1; }
export interface WorkspaceFsAtBindingV1 { openat(input: { readonly parent: NativeWorkspaceFsAtHandleV1|null; readonly name: "/"|NativeWorkspaceFsAtComponentV1; readonly kind: "directory"|"regular_lock"; readonly createMode: 0|0o600 }): NativeWorkspaceFsAtOpenResultV1; mkdirat(parent: NativeWorkspaceFsAtHandleV1,name:NativeWorkspaceFsAtComponentV1,mode:0o700):void; fstatat(parent:NativeWorkspaceFsAtHandleV1,name:NativeWorkspaceFsAtComponentV1):NativeWorkspaceFsAtStatV1; fsyncDirectory(handle:NativeWorkspaceFsAtHandleV1):void; close(handle:NativeWorkspaceFsAtHandleV1):void; fdNumberForSynchronousBorrow(handle:NativeWorkspaceFsAtHandleV1):number; }
export interface WorkspaceFsAtBindingV1 {
openat(input: { readonly parent: NativeWorkspaceFsAtHandleV1|null; readonly name: "/"|NativeWorkspaceFsAtComponentV1; readonly kind: "directory"|"regular_lock"; readonly createMode: 0|0o600 }): NativeWorkspaceFsAtOpenResultV1;
mkdirat(parent: NativeWorkspaceFsAtHandleV1,name:NativeWorkspaceFsAtComponentV1,mode:0o700):void;
fstatat(parent:NativeWorkspaceFsAtHandleV1,name:NativeWorkspaceFsAtComponentV1):NativeWorkspaceFsAtStatV1;
fsyncDirectory(handle:NativeWorkspaceFsAtHandleV1):void;
close(handle:NativeWorkspaceFsAtHandleV1):void;
}
+10 -7
View File
@@ -108,18 +108,20 @@ export class BorrowedWorkspaceSessionReadersExclusiveLockLease {
}
export class WorkspaceWriterLockCapability {
private live = true; private readerExclusive = false;
private constructor(readonly workspaceId: CanonicalWorkspaceId, readonly rootIdentity: WorkspaceLockRootIdentityV1, private readonly root: VerifiedWorkspaceLockRootLease, private readonly fs: WorkspaceFsAtV1, private readonly writer: OwnedWorkspaceFsAtRegularFile) {}
static make(id: CanonicalWorkspaceId, identity: WorkspaceLockRootIdentityV1, root: VerifiedWorkspaceLockRootLease, fs: WorkspaceFsAtV1, writer: OwnedWorkspaceFsAtRegularFile) { return new WorkspaceWriterLockCapability(id, identity, root, fs, writer); }
private constructor(readonly workspaceId: CanonicalWorkspaceId, readonly rootIdentity: WorkspaceLockRootIdentityV1, private readonly root: VerifiedWorkspaceLockRootLease, private readonly writer: import("./workspace-lock-root-lease.js").WorkspaceRootLock) {}
static create(id: CanonicalWorkspaceId, identity: WorkspaceLockRootIdentityV1, root: VerifiedWorkspaceLockRootLease, writer: import("./workspace-lock-root-lease.js").WorkspaceRootLock): WorkspaceWriterLockCapability { return new WorkspaceWriterLockCapability(id, identity, root, writer); }
private check(): void { if (!this.live) throw fail(); }
async runUnderSessionReadersExclusive<T>(action: (lease: BorrowedWorkspaceSessionReadersExclusiveLockLease) => Promise<T>): Promise<T> {
this.check(); if (this.readerExclusive) throw fail(); this.readerExclusive = true;
let lock: OwnedWorkspaceFsAtRegularFile | undefined; let borrowed: BorrowedWorkspaceSessionReadersExclusiveLockLease | undefined; let result!: T;
try { result = await this.root._withRoot(async (dir, fs) => { lock = fs.openOrCreateLockAt(dir, "session-readers.lock", 0o600); fs.flockOwnedLock(lock, "exclusive", "nonblocking"); borrowed = BorrowedWorkspaceSessionReadersExclusiveLockLease.make(this.workspaceId, this.rootIdentity); return action(borrowed); }); return result; }
finally { borrowed?.invalidate(); let failed = false; try { lock?.close(); } catch { failed = true; } if (failed) { this.live = false; } this.readerExclusive = false; if (failed) throw fail(); }
let lock: import("./workspace-lock-root-lease.js").WorkspaceRootLock | undefined; let borrowed: BorrowedWorkspaceSessionReadersExclusiveLockLease | undefined;
try { lock = await this.root.acquireSessionReadersExclusive(); borrowed = BorrowedWorkspaceSessionReadersExclusiveLockLease.make(this.workspaceId, this.rootIdentity); return await action(borrowed); }
catch { throw fail(); }
finally { borrowed?.invalidate(); try { lock?.close(); } catch { this.live = false; } this.readerExclusive = false; }
}
async spawnChild(_request: WorkspaceLockedChildRequest): Promise<WorkspaceLockedChildResult> { this.check(); throw fail("child runner is not configured"); }
async close(): Promise<void> { if (!this.live) return; if (this.readerExclusive) throw fail(); this.live = false; let error: unknown; try { this.writer.close(); } catch (e) { error = e; } try { await this.root.close(); } catch (e) { error ??= e; } if (error) throw fail(); }
}
function makeWriterCapability(id: CanonicalWorkspaceId, identity: WorkspaceLockRootIdentityV1, root: VerifiedWorkspaceLockRootLease, writer: import("./workspace-lock-root-lease.js").WorkspaceRootLock): WorkspaceWriterLockCapability { return WorkspaceWriterLockCapability.create(id, identity, root, writer); }
export interface OrderedWorkspaceCapability { readonly workspaceId: CanonicalWorkspaceId; readonly rootLease: BorrowedVerifiedWorkspaceLockRootLease; readonly writerCapability: WorkspaceWriterLockCapability; }
export class OrderedWorkspaceWriterCapabilitySet {
private live = true; private constructor(private readonly caps: Map<CanonicalWorkspaceId, WorkspaceWriterLockCapability>) {}
@@ -132,9 +134,10 @@ export class OrderedWorkspaceWriterCapabilitySet {
export async function runUnderOrderedWorkspaceWriterLocks<T>(rootLeases: readonly VerifiedWorkspaceLockRootLease[], action: (capabilities: OrderedWorkspaceWriterCapabilitySet) => Promise<T>): Promise<T> {
const sorted = [...rootLeases].sort((a, b) => a.identity.workspaceId.localeCompare(b.identity.workspaceId)); if (new Set(sorted.map(x => x.identity.workspaceId)).size !== sorted.length) throw fail();
const caps: WorkspaceWriterLockCapability[] = [];
try { for (const source of sorted) { const root = source.transfer(); const fs = new WorkspaceFsAtV1(); let cap: WorkspaceWriterLockCapability | undefined; await root._withRoot(async (dir) => { const writer = fs.openOrCreateLockAt(dir, "writer.lock", 0o600); try { fs.flockOwnedLock(writer, "exclusive", "nonblocking"); cap = WorkspaceWriterLockCapability.make(root.identity.workspaceId, root.identity, root, fs, writer); } catch (e) { writer.close(); throw e; } }); if (!cap) throw fail(); caps.push(cap); }
const transferred: VerifiedWorkspaceLockRootLease[] = [];
try { for (const source of sorted) { const root = source.transfer(); transferred.push(root); let writer: import("./workspace-lock-root-lease.js").WorkspaceRootLock | undefined; try { writer = await root.acquireWriterLock(); caps.push(makeWriterCapability(root.identity.workspaceId, root.identity, root, writer)); } catch (error) { try { writer?.close(); } catch {} try { await root.close(); } catch {} throw error; } }
const set = OrderedWorkspaceWriterCapabilitySet.make(new Map(caps.map(c => [c.workspaceId, c]))); try { return await action(set); } finally { set.invalidate(); for (const cap of [...caps].reverse()) await cap.close(); }
} catch (error) { for (const cap of [...caps].reverse()) await cap.close().catch(() => undefined); throw error; }
} catch (error) { for (const cap of [...caps].reverse()) await cap.close().catch(() => undefined); for (const root of transferred.slice(caps.length).reverse()) await root.close().catch(() => undefined); throw error; }
}
export function runUnderWorkspaceWriterLock<T>(rootLease: VerifiedWorkspaceLockRootLease, action: (capability: WorkspaceWriterLockCapability) => Promise<T>) { return runUnderOrderedWorkspaceWriterLocks([rootLease], set => set.forWorkspace(rootLease.identity.workspaceId, x => action(x.writerCapability))); }
export async function probeWorkspaceWriterLock(rootLease: VerifiedWorkspaceLockRootLease): Promise<"available" | "held"> { try { await runUnderWorkspaceWriterLock(rootLease, async () => undefined); return "available"; } catch { return "held"; } }
+62 -20
View File
@@ -1,30 +1,72 @@
import { createRequire } from "node:module";
import type { WorkspaceFsAtBindingV1, NativeWorkspaceFsAtHandleV1, NativeWorkspaceFsAtStatV1, NativeWorkspaceFsAtComponentV1 } from "../native/workspace-fs-at-binding.js";
const require = createRequire(import.meta.url);
const binding: WorkspaceFsAtBindingV1 = require("../../native/workspace-fs-at/build/Release/workspace_fs_at.node");
const binding = require("../../native/workspace-fs-at/build/Release/workspace_fs_at.node") as WorkspaceFsAtBindingV1 & {
withFd(handle: NativeWorkspaceFsAtHandleV1, action: (fd: number) => void): void;
duplicateForChildStdio(writer: NativeWorkspaceFsAtHandleV1, root: NativeWorkspaceFsAtHandleV1, writerFd: number, rootFd: number): void;
};
export interface WorkspaceFsAtStatV1 extends NativeWorkspaceFsAtStatV1 {}
export type LockFileName = "writer.lock" | "session-readers.lock";
export type WorkspaceFlockKindV1 = "shared" | "exclusive";
export type WorkspaceFlockWaitV1 = "blocking" | "nonblocking";
function component(value:string): NativeWorkspaceFsAtComponentV1 { if (typeof value!=="string" || value.length===0 || value.length>255 || value!==value.trim() || value==='.' || value==='..' || value.includes('/') || value.includes('\0')) throw new Error("invalid path component"); if (!isComponent(value)) throw new Error("invalid path component"); return value; }
function isComponent(value:string): value is NativeWorkspaceFsAtComponentV1 { return true; }
function normalizeError(error: unknown): Error { if (error instanceof Error) return error; return new Error(String(error)); }
class Owned {
protected live=true;
constructor(protected readonly raw: NativeWorkspaceFsAtHandleV1, readonly opened: WorkspaceFsAtStatV1) {}
stat(): WorkspaceFsAtStatV1 { if(!this.live) throw new Error("workspace descriptor is closed"); return this.opened; }
close(): void { if(!this.live)return; this.live=false; try { binding.close(this.raw); } catch(e){ throw normalizeError(e); } }
_raw(): NativeWorkspaceFsAtHandleV1 { if(!this.live) throw new Error("workspace descriptor is closed"); return this.raw; }
function component(value: string): NativeWorkspaceFsAtComponentV1 {
if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value, "utf8") > 255 || value !== value.trim() || value === "." || value === ".." || value.includes("/") || value.includes("\0")) throw new Error("invalid path component");
return value as NativeWorkspaceFsAtComponentV1;
}
function normalizeError(error: unknown): Error {
if (error instanceof Error) return error;
return new Error(String(error));
}
const INTERNAL = Symbol("workspace-fs-at-owned");
const rawHandles = new WeakMap<object, NativeWorkspaceFsAtHandleV1>();
const borrowing = new WeakMap<object, number>();
abstract class Owned {
private live = true;
private borrowing = 0;
protected constructor(raw: NativeWorkspaceFsAtHandleV1, readonly opened: WorkspaceFsAtStatV1, token: symbol) { if (token !== INTERNAL) throw new TypeError("private workspace descriptor"); rawHandles.set(this, raw); borrowing.set(this, 0); }
stat(): WorkspaceFsAtStatV1 { if (!this.live) throw new Error("workspace descriptor is closed"); return this.opened; }
close(): void {
if (!this.live) return;
if ((borrowing.get(this) ?? 0) !== 0) throw Object.assign(new Error("workspace descriptor is borrowed"), { code: "ERR_WORKSPACE_FS_AT_BORROWED" });
this.live = false;
try { binding.close(rawHandles.get(this)!); } catch (error) { throw normalizeError(error); }
}
}
export class OwnedWorkspaceFsAtDirectory extends Owned {
constructor(raw: NativeWorkspaceFsAtHandleV1, stat: WorkspaceFsAtStatV1, token: symbol) { super(raw, stat, token); }
}
export class OwnedWorkspaceFsAtRegularFile extends Owned {
constructor(raw: NativeWorkspaceFsAtHandleV1, stat: WorkspaceFsAtStatV1, token: symbol) { super(raw, stat, token); }
}
function wrapDirectory(result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}): OwnedWorkspaceFsAtDirectory {
try { if ((result.openedStat.mode & 0o170000) !== 0o040000) throw new Error("not a directory"); return new OwnedWorkspaceFsAtDirectory(result.handle, result.openedStat, INTERNAL); }
catch (error) { try { binding.close(result.handle); } catch { /* preserve conversion error */ } throw error; }
}
function wrapLock(result: {handle: NativeWorkspaceFsAtHandleV1; openedStat: WorkspaceFsAtStatV1}): OwnedWorkspaceFsAtRegularFile {
try {
const st = result.openedStat;
if ((st.mode & 0o170000) !== 0o100000 || (st.mode & 0o777) !== 0o600 || st.uid !== (process.getuid?.() ?? st.uid) || st.nlink !== 1n) throw new Error("invalid lock identity");
return new OwnedWorkspaceFsAtRegularFile(result.handle, st, INTERNAL);
} catch (error) { try { binding.close(result.handle); } catch { /* preserve conversion error */ } throw error; }
}
function rawDirectory(value: OwnedWorkspaceFsAtDirectory): NativeWorkspaceFsAtHandleV1 { if (!rawHandles.has(value)) throw new Error("workspace descriptor is closed"); return rawHandles.get(value)!; }
function withLockFd<T>(value: OwnedWorkspaceFsAtRegularFile, action: (fd: number) => T): T {
if (!rawHandles.has(value)) throw new Error("workspace descriptor is closed");
const count = borrowing.get(value) ?? 0; borrowing.set(value, count + 1);
try { return binding.withFd(rawHandles.get(value)!, action as (fd: number) => void) as T; } finally { borrowing.set(value, count); }
}
export class OwnedWorkspaceFsAtDirectory extends Owned { private constructor(raw:NativeWorkspaceFsAtHandleV1, stat:WorkspaceFsAtStatV1){super(raw,stat);} static from(raw:NativeWorkspaceFsAtHandleV1,stat:WorkspaceFsAtStatV1){return new OwnedWorkspaceFsAtDirectory(raw,stat);} }
export class OwnedWorkspaceFsAtRegularFile extends Owned { private constructor(raw:NativeWorkspaceFsAtHandleV1, stat:WorkspaceFsAtStatV1){super(raw,stat);} static from(raw:NativeWorkspaceFsAtHandleV1,stat:WorkspaceFsAtStatV1){return new OwnedWorkspaceFsAtRegularFile(raw,stat);} }
function rawDirectory(value:OwnedWorkspaceFsAtDirectory){ return value._raw(); }
export class WorkspaceFsAtV1 {
openRoot(): OwnedWorkspaceFsAtDirectory { const r=binding.openat({parent:null,name:"/",kind:"directory",createMode:0}); return OwnedWorkspaceFsAtDirectory.from(r.handle,r.openedStat); }
openDirectoryAt(parent:OwnedWorkspaceFsAtDirectory, name:string):OwnedWorkspaceFsAtDirectory { const r=binding.openat({parent:rawDirectory(parent),name:component(name),kind:"directory",createMode:0}); return OwnedWorkspaceFsAtDirectory.from(r.handle,r.openedStat); }
openOrCreateLockAt(parent:OwnedWorkspaceFsAtDirectory,name:LockFileName,mode:0o600):OwnedWorkspaceFsAtRegularFile { if(name!=="writer.lock"&&name!=="session-readers.lock"||mode!==0o600)throw new Error("invalid lock"); const r=binding.openat({parent:rawDirectory(parent),name:component(name),kind:"regular_lock",createMode:0o600}); return OwnedWorkspaceFsAtRegularFile.from(r.handle,r.openedStat); }
mkdirAt(parent:OwnedWorkspaceFsAtDirectory,name:string,mode:0o700):void { binding.mkdirat(rawDirectory(parent),component(name),mode); }
statAtNoFollow(parent:OwnedWorkspaceFsAtDirectory,name:string):WorkspaceFsAtStatV1 { return binding.fstatat(rawDirectory(parent),component(name)); }
fsyncDirectory(directory:OwnedWorkspaceFsAtDirectory):void { binding.fsyncDirectory(rawDirectory(directory)); }
flockOwnedLock(owned:OwnedWorkspaceFsAtRegularFile,kind:WorkspaceFlockKindV1,wait:WorkspaceFlockWaitV1):void { const fd=binding.fdNumberForSynchronousBorrow(owned._raw()); const fsExt: {flockSync(fd:number,operation:string):void}=require("fs-ext"); const operation=kind==="shared"?(wait==="blocking"?"sh":"shnb"):(wait==="blocking"?"ex":"exnb"); fsExt.flockSync(fd,operation); }
openRoot(): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({ parent: null, name: "/", kind: "directory", createMode: 0 })); }
openDirectoryAt(parent: OwnedWorkspaceFsAtDirectory, name: string): OwnedWorkspaceFsAtDirectory { return wrapDirectory(binding.openat({ parent: rawDirectory(parent), name: component(name), kind: "directory", createMode: 0 })); }
openOrCreateLockAt(parent: OwnedWorkspaceFsAtDirectory, name: LockFileName, mode: 0o600): OwnedWorkspaceFsAtRegularFile { if ((name !== "writer.lock" && name !== "session-readers.lock") || mode !== 0o600) throw new Error("invalid lock"); return wrapLock(binding.openat({ parent: rawDirectory(parent), name: component(name), kind: "regular_lock", createMode: 0o600 })); }
mkdirAt(parent: OwnedWorkspaceFsAtDirectory, name: string, mode: 0o700): void { binding.mkdirat(rawDirectory(parent), component(name), mode); }
statAtNoFollow(parent: OwnedWorkspaceFsAtDirectory, name: string): WorkspaceFsAtStatV1 { return binding.fstatat(rawDirectory(parent), component(name)); }
fsyncDirectory(directory: OwnedWorkspaceFsAtDirectory): void { binding.fsyncDirectory(rawDirectory(directory)); }
flockOwnedLock(owned: OwnedWorkspaceFsAtRegularFile, kind: WorkspaceFlockKindV1, wait: WorkspaceFlockWaitV1): void {
const fsExt: { flockSync(fd: number, operation: string): void } = require("fs-ext");
const operation = kind === "shared" ? (wait === "blocking" ? "sh" : "shnb") : (wait === "blocking" ? "ex" : "exnb");
withLockFd(owned, fd => fsExt.flockSync(fd, operation));
}
}
@@ -1,41 +1,79 @@
import { WorkspaceFsAtV1, OwnedWorkspaceFsAtDirectory, type OwnedWorkspaceFsAtRegularFile, type WorkspaceFsAtStatV1 } from "./workspace-fs-at.js";
import { lstatSync, realpathSync } from "node:fs";
import { resolve } from "node:path";
import { WorkspaceFsAtV1, OwnedWorkspaceFsAtDirectory, type OwnedWorkspaceFsAtRegularFile, type WorkspaceFsAtStatV1, type WorkspaceFlockKindV1, type WorkspaceFlockWaitV1 } from "./workspace-fs-at.js";
export type CanonicalWorkspaceId = string & { readonly __workspaceId: unique symbol };
export interface WorkspaceLockRootIdentityV1 { readonly schemaVersion:1; readonly workspaceId:CanonicalWorkspaceId; readonly device:bigint; readonly inode:bigint; }
export class CanonicalWorkspaceLockRootInput { private constructor(readonly workspaceId:CanonicalWorkspaceId, readonly owner:symbol){} static make(id:CanonicalWorkspaceId,owner:symbol){return new CanonicalWorkspaceLockRootInput(id,owner);} }
export class BorrowedVerifiedWorkspaceLockRootLease { private constructor(readonly identity:WorkspaceLockRootIdentityV1, private readonly check:()=>void){} static make(i:WorkspaceLockRootIdentityV1,c:()=>void){return new BorrowedVerifiedWorkspaceLockRootLease(i,c);} assertLive(){this.check();} }
function conflict(message="preprocessing conflict"):Error { const e=new Error(message); e.name="PreprocessingConflictError"; return e; }
function exactStat(stat:WorkspaceFsAtStatV1,uid:number):boolean { return (stat.mode&0o170000)===0o040000 && (stat.mode&0o777)===0o700 && stat.uid===uid && stat.nlink>=2n; }
export interface WorkspaceLockRootIdentityV1 { readonly schemaVersion: 1; readonly workspaceId: CanonicalWorkspaceId; readonly device: bigint; readonly inode: bigint; }
export class CanonicalWorkspaceLockRootInput { private constructor(readonly workspaceId: CanonicalWorkspaceId, readonly owner: symbol) {} }
function conflict(message = "preprocessing_conflict"): Error { const e = new Error(message); e.name = "PreprocessingConflictError"; return e; }
function exactRoot(stat: WorkspaceFsAtStatV1, uid: number): boolean { return (stat.mode & 0o170000) === 0o040000 && (stat.mode & 0o777) === 0o700 && stat.uid === uid && stat.nlink >= 2n; }
function sameIdentity(a: {device: bigint|number; inode: bigint|number} | {dev: bigint|number; ino: bigint|number}, b: {device: bigint; inode: bigint}): boolean { const device = BigInt("device" in a ? a.device : a.dev); const inode = BigInt("inode" in a ? a.inode : a.ino); return device === b.device && inode === b.inode; }
/** Internal opaque lock returned only after the root has been checked. */
export class WorkspaceRootLock {
private live = true;
constructor(private readonly fs: WorkspaceFsAtV1, private readonly lock: OwnedWorkspaceFsAtRegularFile) {}
flock(kind: WorkspaceFlockKindV1, wait: WorkspaceFlockWaitV1): void { if (!this.live) throw conflict(); this.fs.flockOwnedLock(this.lock, kind, wait); }
close(): void { if (!this.live) return; this.live = false; this.lock.close(); }
}
export class BorrowedVerifiedWorkspaceLockRootLease {
private constructor(readonly identity: WorkspaceLockRootIdentityV1, private readonly check: () => void) {}
static make(identity: WorkspaceLockRootIdentityV1, check: () => void): BorrowedVerifiedWorkspaceLockRootLease { return new BorrowedVerifiedWorkspaceLockRootLease(identity, check); }
assertLive(): void { this.check(); }
}
export class WorkspaceSessionReadersLockLease {
private live=true; private constructor(private readonly lock:OwnedWorkspaceFsAtRegularFile, private readonly root:OwnedWorkspaceFsAtDirectory, readonly rootIdentity:WorkspaceLockRootIdentityV1){}
static make(lock:OwnedWorkspaceFsAtRegularFile,root:OwnedWorkspaceFsAtDirectory,id:WorkspaceLockRootIdentityV1){return new WorkspaceSessionReadersLockLease(lock,root,id);}
transfer():WorkspaceSessionReadersLockLease { if(!this.live)throw conflict(); this.live=false; return WorkspaceSessionReadersLockLease.make(this.lock,this.root,this.rootIdentity); }
async close():Promise<void>{if(!this.live)return;this.live=false;let failure:unknown;try{this.lock.close();}catch(e){failure=e;}try{this.root.close();}catch(e){failure??=e;}if(failure)throw conflict();}
private live = true;
private constructor(private readonly lock: WorkspaceRootLock, private readonly root: OwnedWorkspaceFsAtDirectory, readonly rootIdentity: WorkspaceLockRootIdentityV1) {}
transfer(): WorkspaceSessionReadersLockLease { if (!this.live) throw conflict(); this.live = false; return new WorkspaceSessionReadersLockLease(this.lock, this.root, this.rootIdentity); }
async close(): Promise<void> { if (!this.live) return; this.live = false; let failure: unknown; try { this.lock.close(); } catch (e) { failure = e; } try { this.root.close(); } catch (e) { failure ??= e; } if (failure) throw conflict(); }
static from(lock: WorkspaceRootLock, root: OwnedWorkspaceFsAtDirectory, identity: WorkspaceLockRootIdentityV1): WorkspaceSessionReadersLockLease { return new WorkspaceSessionReadersLockLease(lock, root, identity); }
}
export class VerifiedWorkspaceLockRootLease {
private live=true; private borrowed=0; private constructor(private readonly owner:symbol,private readonly fs:WorkspaceFsAtV1,private readonly root:OwnedWorkspaceFsAtDirectory,readonly identity:WorkspaceLockRootIdentityV1){}
static make(owner:symbol,fs:WorkspaceFsAtV1,root:OwnedWorkspaceFsAtDirectory,id:WorkspaceLockRootIdentityV1){return new VerifiedWorkspaceLockRootLease(owner,fs,root,id);}
private check(){if(!this.live)throw conflict(); const s=this.root.stat();if(s.device!==this.identity.device||s.inode!==this.identity.inode)throw conflict("workspace root identity changed");}
async borrow<T>(action:(b:BorrowedVerifiedWorkspaceLockRootLease)=>Promise<T>):Promise<T>{this.check();this.borrowed++;try{return await action(BorrowedVerifiedWorkspaceLockRootLease.make(this.identity,()=>this.check()));}finally{this.borrowed--;}}
transfer():VerifiedWorkspaceLockRootLease{this.check();if(this.borrowed)throw conflict("workspace root is borrowed");this.live=false;return VerifiedWorkspaceLockRootLease.make(this.owner,this.fs,this.root,this.identity);}
async acquireSessionReadersShared():Promise<WorkspaceSessionReadersLockLease>{this.check();let lock:OwnedWorkspaceFsAtRegularFile|undefined;try{lock=this.fs.openOrCreateLockAt(this.root,"session-readers.lock",0o600);this.fs.flockOwnedLock(lock,"shared","nonblocking");this.check();this.live=false;return WorkspaceSessionReadersLockLease.make(lock,this.root,this.identity);}catch(e){try{lock?.close();}catch{this.live=false;try{this.root.close();}catch{}}throw conflict();}}
async close():Promise<void>{if(!this.live)return;if(this.borrowed)await new Promise<void>(resolve=>{const tick=()=>this.borrowed?setTimeout(tick,1):resolve();tick();});this.live=false;try{this.root.close();}catch{throw conflict();}}
// package-private operation used by the writer capability; never returns the underlying handle.
async _withRoot<T>(action:(root:OwnedWorkspaceFsAtDirectory,fs:WorkspaceFsAtV1)=>Promise<T>):Promise<T>{this.check();return action(this.root,this.fs);}
private live = true;
private borrowed = 0;
private constructor(private readonly owner: symbol, private readonly fs: WorkspaceFsAtV1, private readonly root: OwnedWorkspaceFsAtDirectory, readonly identity: WorkspaceLockRootIdentityV1, private readonly rootPath: string, private readonly serviceUid: number) {}
private assertPath(): void {
if (!this.live) throw conflict();
try { const st = lstatSync(this.rootPath); if (!st.isDirectory() || st.uid !== this.serviceUid || (st.mode & 0o777) !== 0o700 || !sameIdentity(st, this.identity)) throw conflict("preprocessing_conflict: workspace root identity changed"); }
catch (error) { if ((error as Error).name === "PreprocessingConflictError") throw error; throw conflict("preprocessing_conflict: workspace root identity changed"); }
const retained = this.root.stat(); if (!sameIdentity(retained, this.identity) || !exactRoot(retained, this.serviceUid)) throw conflict("preprocessing_conflict: workspace root identity changed");
}
assertLive(): void { this.assertPath(); }
async borrow<T>(action: (lease: { readonly identity: WorkspaceLockRootIdentityV1; assertLive(): void }) => Promise<T>): Promise<T> { this.assertPath(); this.borrowed++; try { return await action({ identity: this.identity, assertLive: () => this.assertPath() }); } finally { this.borrowed--; } }
transfer(): VerifiedWorkspaceLockRootLease { this.assertPath(); if (this.borrowed) throw conflict("workspace root is borrowed"); this.live = false; return new VerifiedWorkspaceLockRootLease(this.owner, this.fs, this.root, this.identity, this.rootPath, this.serviceUid); }
async acquireSessionReadersShared(): Promise<WorkspaceSessionReadersLockLease> {
this.assertPath(); let lock: WorkspaceRootLock | undefined;
try { const owned = this.fs.openOrCreateLockAt(this.root, "session-readers.lock", 0o600); lock = new WorkspaceRootLock(this.fs, owned); lock.flock("shared", "nonblocking"); this.assertPath(); this.live = false; return WorkspaceSessionReadersLockLease.from(lock, this.root, this.identity); }
catch (error) { try { lock?.close(); } catch { this.live = false; try { this.root.close(); } catch {} } throw conflict(); }
}
async acquireWriterLock(): Promise<WorkspaceRootLock> { this.assertPath(); const owned = this.fs.openOrCreateLockAt(this.root, "writer.lock", 0o600); const lock = new WorkspaceRootLock(this.fs, owned); try { lock.flock("exclusive", "nonblocking"); this.assertPath(); return lock; } catch (error) { try { lock.close(); } catch {} throw conflict(); } }
async acquireSessionReadersExclusive(): Promise<WorkspaceRootLock> { this.assertPath(); const owned = this.fs.openOrCreateLockAt(this.root, "session-readers.lock", 0o600); const lock = new WorkspaceRootLock(this.fs, owned); try { lock.flock("exclusive", "nonblocking"); this.assertPath(); return lock; } catch { try { lock.close(); } catch {} throw conflict(); } }
fsync(): void { this.assertPath(); this.fs.fsyncDirectory(this.root); this.assertPath(); }
async close(): Promise<void> { if (!this.live) return; while (this.borrowed) await new Promise<void>(resolve => setTimeout(resolve, 1)); this.live = false; try { this.root.close(); } catch { throw conflict(); } }
static from(owner: symbol, fs: WorkspaceFsAtV1, root: OwnedWorkspaceFsAtDirectory, identity: WorkspaceLockRootIdentityV1, rootPath: string, uid: number): VerifiedWorkspaceLockRootLease { return new VerifiedWorkspaceLockRootLease(owner, fs, root, identity, rootPath, uid); }
}
export class VerifiedWorkspaceLockRootLeaseFactory {
private readonly owner=Symbol(); private readonly parent:OwnedWorkspaceFsAtDirectory;
constructor(private readonly input:{readonly workspaceFsAt:WorkspaceFsAtV1;readonly installationId:string;readonly sessionsRootFromValidatedInstallationConfig:string;readonly serviceUid:number;readonly provisionedWorkspaceMode:0o700}) { if(!Number.isInteger(input.serviceUid)||input.serviceUid<0)throw new Error("invalid service uid");if(input.provisionedWorkspaceMode!==0o700)throw new Error("invalid workspace mode");if(!input.sessionsRootFromValidatedInstallationConfig.startsWith("/"))throw new Error("sessions root must be absolute");let d=input.workspaceFsAt.openRoot();try{for(const c of input.sessionsRootFromValidatedInstallationConfig.split("/").filter(Boolean)) {const n=input.workspaceFsAt.openDirectoryAt(d,c);d.close();d=n;}this.parent=d;}catch(e){try{d.close();}catch{}throw e;}}
canonicalInput(workspaceId:string):CanonicalWorkspaceLockRootInput{if(!/^[a-z][a-z0-9-]{2,62}$/.test(workspaceId))throw conflict();return CanonicalWorkspaceLockRootInput.make(workspaceId as CanonicalWorkspaceId,this.owner);}
private async open(input:CanonicalWorkspaceLockRootInput):Promise<VerifiedWorkspaceLockRootLease>{if(input.owner!==this.owner)throw conflict();const root=this.input.workspaceFsAt.openDirectoryAt(this.parent,input.workspaceId);if(!exactStat(root.stat(),this.input.serviceUid)){root.close();throw conflict();}return VerifiedWorkspaceLockRootLease.make(this.owner,this.input.workspaceFsAt,root,{schemaVersion:1,workspaceId:input.workspaceId,device:root.stat().device,inode:root.stat().inode});}
acquire(input:CanonicalWorkspaceLockRootInput){return this.open(input);}
async acquireOrProvision(input:CanonicalWorkspaceLockRootInput){
if(input.owner!==this.owner)throw conflict();
try{return await this.open(input);}catch(error){if((error as {code?:unknown})?.code!=="ENOENT")throw conflict();}
try{this.input.workspaceFsAt.mkdirAt(this.parent,input.workspaceId,0o700);}catch(error){if((error as {code?:unknown})?.code!=="EEXIST")throw conflict();}
try{this.input.workspaceFsAt.fsyncDirectory(this.parent);}catch{throw conflict();}
const lease=await this.open(input);
try{await lease._withRoot(async(dir,fs)=>fs.fsyncDirectory(dir));}
catch{await lease.close().catch(()=>undefined);throw conflict();}
return lease;
private readonly owner = Symbol("workspace-root-factory");
private readonly parent: OwnedWorkspaceFsAtDirectory;
private readonly parentPath: string;
constructor(private readonly input: { readonly workspaceFsAt: WorkspaceFsAtV1; readonly installationId: string; readonly sessionsRootFromValidatedInstallationConfig: string; readonly serviceUid: number; readonly provisionedWorkspaceMode: 0o700 }) {
if (!Number.isInteger(input.serviceUid) || input.serviceUid < 0 || input.provisionedWorkspaceMode !== 0o700) throw new Error("invalid workspace root policy");
try { const configured = resolve(input.sessionsRootFromValidatedInstallationConfig); this.parentPath = realpathSync(configured); if (this.parentPath !== configured) throw conflict("sessions root must be canonical"); } catch (error) { if ((error as Error).name === "PreprocessingConflictError") throw error; throw conflict("invalid sessions root"); }
if (!this.parentPath.startsWith("/") || this.parentPath.split("/").includes("..")) throw conflict("invalid sessions root");
let d = input.workspaceFsAt.openRoot();
try { for (const c of this.parentPath.split("/").filter(Boolean)) { const n = input.workspaceFsAt.openDirectoryAt(d, c); d.close(); d = n; } this.parent = d; this.checkParent(); }
catch (error) { try { d.close(); } catch {} throw error; }
}
private checkParent(): void { try { const s = lstatSync(this.parentPath); const p = this.parent.stat(); if (!s.isDirectory() || !sameIdentity(s, p) || s.uid !== this.input.serviceUid || (s.mode & 0o777) !== 0o700) throw conflict("installation root identity changed"); } catch (error) { if ((error as Error).name === "PreprocessingConflictError") throw error; throw conflict("installation root identity changed"); } }
canonicalInput(workspaceId: string): CanonicalWorkspaceLockRootInput { if (!/^[a-z][a-z0-9-]{2,62}$/.test(workspaceId)) throw conflict(); return Object.assign(Object.create(CanonicalWorkspaceLockRootInput.prototype), { workspaceId: workspaceId as CanonicalWorkspaceId, owner: this.owner }) as CanonicalWorkspaceLockRootInput; }
private async open(input: CanonicalWorkspaceLockRootInput): Promise<VerifiedWorkspaceLockRootLease> { if (input.owner !== this.owner) throw conflict(); this.checkParent(); let root: OwnedWorkspaceFsAtDirectory; try { root = this.input.workspaceFsAt.openDirectoryAt(this.parent, input.workspaceId); this.checkParent(); } catch { throw conflict(); } if (!exactRoot(root.stat(), this.input.serviceUid)) { root.close(); throw conflict(); } const identity = { schemaVersion: 1 as const, workspaceId: input.workspaceId, device: root.stat().device, inode: root.stat().inode }; const lease = VerifiedWorkspaceLockRootLease.from(this.owner, this.input.workspaceFsAt, root, identity, `${this.parentPath}/${input.workspaceId}`, this.input.serviceUid); try { lease.assertLive(); } catch { await lease.close().catch(() => undefined); throw conflict(); } return lease; }
acquire(input: CanonicalWorkspaceLockRootInput): Promise<VerifiedWorkspaceLockRootLease> { return this.open(input); }
async acquireOrProvision(input: CanonicalWorkspaceLockRootInput): Promise<VerifiedWorkspaceLockRootLease> {
if (input.owner !== this.owner) throw conflict();
try { return await this.open(input); } catch (error) { if (!String((error as Error).message).includes("identity") && (error as {code?: string}).code !== "ENOENT") { /* open errors are normalized; probe the anchored parent below */ } }
this.checkParent();
try { this.input.workspaceFsAt.mkdirAt(this.parent, input.workspaceId, 0o700); } catch (error) { if ((error as {code?: string}).code !== "EEXIST") throw conflict(); }
this.checkParent();
try { this.input.workspaceFsAt.fsyncDirectory(this.parent); } catch { throw conflict(); }
return this.open(input);
}
}
+5 -1
View File
@@ -1 +1,5 @@
process.stdout.write(JSON.stringify({ok:true}));
import { createRequire } from "node:module"; import { resolve } from "node:path";
const require = createRequire(import.meta.url); const a = require(resolve(process.cwd(), "native/workspace-fs-at/build/Release/workspace_fs_at.node"));
let root = a.openat({parent:null,name:"/",kind:"directory",createMode:0}).handle; let successes=0, failures=0;
for (let i=0;i<Number(process.env.ITERATIONS??100);i++) { try { const r=a.openat({parent:root,name:process.env.COMPONENT??"tmp",kind:"directory",createMode:0}); successes++; a.close(r.handle); } catch { failures++; } }
a.close(root); process.stdout.write(JSON.stringify({ok:true,successes,failures}));
+14 -1
View File
@@ -1 +1,14 @@
process.stdout.write(JSON.stringify({ok:true}));
import { createRequire } from "node:module";
import { setTimeout as sleep } from "node:timers/promises";
import { resolve } from "node:path";
const require = createRequire(import.meta.url);
const addon = require(resolve(process.cwd(), "native/workspace-fs-at/build/Release/workspace_fs_at.node"));
const fsExt = require("fs-ext");
let root = addon.openat({ parent: null, name: "/", kind: "directory", createMode: 0 }).handle;
for (const p of (process.env.WORKSPACE_ROOT ?? "").split("/").filter(Boolean)) root = addon.openat({ parent: root, name: p, kind: "directory", createMode: 0 }).handle;
const lockName = process.env.LOCK_NAME ?? "session-readers.lock";
const lock = addon.openat({ parent: root, name: lockName, kind: "regular_lock", createMode: 0o600 }).handle;
addon.withFd(lock, fd => fsExt.flockSync(fd, process.env.LOCK_MODE ?? "exnb"));
process.stdout.write(JSON.stringify({ ready: true }));
await sleep(Number(process.env.HOLD_MS ?? 100));
addon.close(lock); addon.close(root);
@@ -1 +1 @@
process.stdout.write(JSON.stringify({ok:true}));
import { mkdir, writeFile } from "node:fs/promises"; const root=process.env.JOB_ROOT; if (!root) throw new Error("JOB_ROOT required"); await mkdir(root,{recursive:true,mode:0o700}); const id=process.env.RUN_ID??"a".repeat(32); await writeFile(`${root}/${id}.json`,JSON.stringify({runId:id,phase:"request_claimed"})+"\n",{flag:"wx",mode:0o600}); process.stdout.write(JSON.stringify({ok:true,runId:id}));
+14 -1
View File
@@ -1 +1,14 @@
process.stdout.write(JSON.stringify({ok:true}));
import { createRequire } from "node:module";
import { setTimeout as sleep } from "node:timers/promises";
import { resolve } from "node:path";
const require = createRequire(import.meta.url);
const addon = require(resolve(process.cwd(), "native/workspace-fs-at/build/Release/workspace_fs_at.node"));
const fsExt = require("fs-ext");
let root = addon.openat({ parent: null, name: "/", kind: "directory", createMode: 0 }).handle;
for (const p of (process.env.WORKSPACE_ROOT ?? "").split("/").filter(Boolean)) root = addon.openat({ parent: root, name: p, kind: "directory", createMode: 0 }).handle;
const lockName = process.env.LOCK_NAME ?? "session-readers.lock";
const lock = addon.openat({ parent: root, name: lockName, kind: "regular_lock", createMode: 0o600 }).handle;
addon.withFd(lock, fd => fsExt.flockSync(fd, process.env.LOCK_MODE ?? "exnb"));
process.stdout.write(JSON.stringify({ ready: true }));
await sleep(Number(process.env.HOLD_MS ?? 100));
addon.close(lock); addon.close(root);
+38 -3
View File
@@ -1,4 +1,39 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, afterEach } from "vitest";
import { mkdtempSync, mkdirSync, chmodSync, writeFileSync, linkSync, symlinkSync, rmSync } from "node:fs";
import { join } from "node:path";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
import { WorkspaceFsAtV1 } from "../src/workspaces/workspace-fs-at.js";
import { mkdtempSync, mkdirSync, statSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path";
describe("workspace fs-at seam",()=>{it("opens anchored directories and literal locks",()=>{const root=mkdtempSync(join(process.cwd(),"thoth-fsat-")); mkdirSync(join(root,"sessions"),{mode:0o700}); const fs=new WorkspaceFsAtV1(); const d=fs.openRoot(); const s=fs.openDirectoryAt(d,"private"); expect(s.stat().mode).toBeTruthy(); s.close(); d.close();});});
const roots: string[] = [];
function openAbsolute(fs: WorkspaceFsAtV1, path: string) { let d = fs.openRoot(); for (const component of path.split("/").filter(Boolean)) { const next = fs.openDirectoryAt(d, component); d.close(); d = next; } return d; }
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
describe("workspace fs-at native seam", () => {
it("exposes only opaque filesystem operations and preserves exact stat identity", () => {
const root = mkdtempSync(join(process.cwd(), "thoth-fsat-")); roots.push(root); mkdirSync(join(root, "private"), { mode: 0o700 });
const fs = new WorkspaceFsAtV1(); const d = openAbsolute(fs, root); const child = fs.openDirectoryAt(d, "private");
expect(child.stat().mode & 0o170000).toBe(0o040000);
expect(Object.keys(require("../native/workspace-fs-at/build/Release/workspace_fs_at.node"))).toEqual(["openat", "mkdirat", "fstatat", "fsyncDirectory", "close"]);
expect((child as unknown as Record<string, unknown>)._raw).toBeUndefined();
child.close(); d.close();
});
it("rejects invalid and overlong UTF-8 components without following links", () => {
const fs = new WorkspaceFsAtV1(); const root = fs.openRoot();
for (const name of ["", ".", "..", "a/b", "a\0b", "é".repeat(128)]) expect(() => fs.openDirectoryAt(root, name)).toThrow();
expect(() => fs.openDirectoryAt(root, "does-not-exist")).toThrow(); root.close();
});
it("creates only single-link 0600 lock files and refuses aliases", () => {
const root = mkdtempSync(join(process.cwd(), "thoth-fsat-")); roots.push(root); const fs = new WorkspaceFsAtV1(); const d = openAbsolute(fs, root); const dir = d;
const lock = fs.openOrCreateLockAt(dir, "writer.lock", 0o600); expect(lock.stat().mode & 0o777).toBe(0o600); expect(lock.stat().nlink).toBe(1n); lock.close(); dir.close(); d.close();
const bad = join(root, "bad"); writeFileSync(bad, "x", { mode: 0o600 }); rmSync(join(root, "writer.lock")); linkSync(bad, join(root, "writer.lock"));
const d2 = openAbsolute(fs, root); expect(() => fs.openOrCreateLockAt(d2, "writer.lock", 0o600)).toThrow(); d2.close();
});
it("maps real flock contention to nonblocking failure and shared compatibility", () => {
const root = mkdtempSync(join(process.cwd(), "thoth-fsat-")); roots.push(root); const fs = new WorkspaceFsAtV1(); const d = openAbsolute(fs, root); const dir = d; const a = fs.openOrCreateLockAt(dir, "writer.lock", 0o600); const b = fs.openOrCreateLockAt(dir, "writer.lock", 0o600);
fs.flockOwnedLock(a, "exclusive", "nonblocking"); expect(() => fs.flockOwnedLock(b, "shared", "nonblocking")).toThrow(); a.close(); fs.flockOwnedLock(b, "exclusive", "nonblocking"); b.close(); dir.close(); d.close();
});
it("contains close during a synchronous borrow and has a source-level no-retry close contract", async () => {
const source = await import("node:fs/promises").then(() => require("node:fs").readFileSync("native/workspace-fs-at/workspace_fs_at.cc", "utf8"));
expect(source).toContain("ERR_WORKSPACE_FS_AT_CLOSE_UNCERTAIN"); expect(source).not.toMatch(/do\s*\{[^}]*close\([^)]*\)[^}]*\}\s*while[^;]*EINTR/s);
});
});
+30 -2
View File
@@ -1,2 +1,30 @@
import { describe, expect, it } from "vitest"; import { mkdtempSync,mkdirSync } from "node:fs"; import {join} from "node:path"; import {tmpdir,getuid} from "node:os"; import {WorkspaceFsAtV1} from "../src/workspaces/workspace-fs-at.js"; import {VerifiedWorkspaceLockRootLeaseFactory} from "../src/workspaces/workspace-lock-root-lease.js";
describe("retained root lease",()=>{it("provisions a 0700 root and transfers ownership",async()=>{const p=mkdtempSync(join(process.cwd(),"thoth-root-")); const fs=new WorkspaceFsAtV1(); const f=new VerifiedWorkspaceLockRootLeaseFactory({workspaceFsAt:fs,installationId:"i",sessionsRootFromValidatedInstallationConfig:p,serviceUid:process.getuid!(),provisionedWorkspaceMode:0o700}); const l=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); expect(l.identity.workspaceId).toBe("abc-workspace"); const t=l.transfer(); await expect(l.close()).resolves.toBeUndefined(); await t.close();});});
import { describe, expect, it, afterEach } from "vitest";
import { mkdtempSync, renameSync, mkdirSync, rmSync, statSync, chmodSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { WorkspaceFsAtV1 } from "../src/workspaces/workspace-fs-at.js";
import { VerifiedWorkspaceLockRootLeaseFactory } from "../src/workspaces/workspace-lock-root-lease.js";
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
function factory(sessionsRootFromValidatedInstallationConfig: string) { return new VerifiedWorkspaceLockRootLeaseFactory({ workspaceFsAt: new WorkspaceFsAtV1(), installationId: "i", sessionsRootFromValidatedInstallationConfig, serviceUid: process.getuid!(), provisionedWorkspaceMode: 0o700 }); }
describe("retained canonical workspace root", () => {
it("provisions exact identity, transfers once, and rejects a second owner", async () => {
const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const f = factory(parent); const input = f.canonicalInput("abc-workspace");
const lease = await f.acquireOrProvision(input); const st = statSync(join(parent, "abc-workspace")); expect(st.uid).toBe(process.getuid!()); expect(st.mode & 0o777).toBe(0o700); expect(lease.identity.inode).toBe(BigInt(st.ino));
const transferred = lease.transfer(); expect(() => lease.transfer()).toThrow(); await transferred.close();
expect(() => f.canonicalInput("../outside")).toThrow(); expect(() => f.canonicalInput("/tmp/x")).toThrow();
});
it("fails closed when the canonical pathname is replaced after retention", async () => {
const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const f = factory(parent); const lease = await f.acquireOrProvision(f.canonicalInput("abc-workspace"));
renameSync(join(parent, "abc-workspace"), join(parent, "old")); mkdirSync(join(parent, "abc-workspace"), { mode: 0o700 });
await expect(lease.acquireWriterLock()).rejects.toThrow(/preprocessing/); await lease.close();
});
it("does not accept a symlink or wrong ownership/mode root", async () => {
const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const other = mkdtempSync(join(process.cwd(), "thoth-other-")); roots.push(other); symlinkSync(other, join(parent, "abc-workspace"));
const f = factory(parent); await expect(f.acquireOrProvision(f.canonicalInput("abc-workspace"))).rejects.toThrow(); rmSync(join(parent, "abc-workspace")); mkdirSync(join(parent, "abc-workspace"), { mode: 0o755 }); await expect(f.acquireOrProvision(f.canonicalInput("abc-workspace"))).rejects.toThrow();
});
it("closes every transferred root on partial ordered acquisition", async () => {
const parent = mkdtempSync(join(process.cwd(), "thoth-root-")); roots.push(parent); const f = factory(parent); const a = await f.acquireOrProvision(f.canonicalInput("aaa-workspace")); const b = await f.acquireOrProvision(f.canonicalInput("bbb-workspace"));
const { runUnderOrderedWorkspaceWriterLocks } = await import("../src/workspaces/preprocessing-state.js"); await runUnderOrderedWorkspaceWriterLocks([a, b], async set => { expect(set.workspaceIds).toEqual(["aaa-workspace", "bbb-workspace"]); }); await expect(b.close()).resolves.toBeUndefined();
});
});
@@ -1,2 +1,17 @@
import {describe,expect,it} from "vitest"; import {mkdtempSync} from "node:fs"; import {join} from "node:path"; import {tmpdir} from "node:os"; import {WorkspaceFsAtV1} from "../src/workspaces/workspace-fs-at.js"; import {VerifiedWorkspaceLockRootLeaseFactory} from "../src/workspaces/workspace-lock-root-lease.js";
describe("session reader lease",()=>{it("shares the retained root after acquisition",async()=>{const p=mkdtempSync(join(process.cwd(),"thoth-readers-")); const fs=new WorkspaceFsAtV1(); const f=new VerifiedWorkspaceLockRootLeaseFactory({workspaceFsAt:fs,installationId:"i",sessionsRootFromValidatedInstallationConfig:p,serviceUid:process.getuid!(),provisionedWorkspaceMode:0o700}); const l=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const r=await l.acquireSessionReadersShared(); expect(r.rootIdentity.workspaceId).toBe("abc-workspace"); await r.close();});});
import { describe, expect, it, afterEach } from "vitest";
import { mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { spawn } from "node:child_process"; import { once } from "node:events";
import { WorkspaceFsAtV1 } from "../src/workspaces/workspace-fs-at.js"; import { VerifiedWorkspaceLockRootLeaseFactory } from "../src/workspaces/workspace-lock-root-lease.js";
const roots: string[] = []; afterEach(() => { for (const r of roots.splice(0)) rmSync(r, { recursive:true, force:true }); });
function factory(root:string) { return new VerifiedWorkspaceLockRootLeaseFactory({workspaceFsAt:new WorkspaceFsAtV1(),installationId:"i",sessionsRootFromValidatedInstallationConfig:root,serviceUid:process.getuid!(),provisionedWorkspaceMode:0o700}); }
describe("session reader lease", () => {
it("holds a real shared flock across child lifetime and releases exactly once", async () => {
const parent=mkdtempSync(join(process.cwd(),"thoth-readers-")); roots.push(parent); const f=factory(parent); const lease=await f.acquireOrProvision(f.canonicalInput("abc-workspace"));
const shared=await lease.acquireSessionReadersShared(); const child=spawn(process.execPath,[join(process.cwd(),"test/fixtures/workspace-session-readers-worker.mjs")],{cwd:process.cwd(),env:{...process.env,WORKSPACE_ROOT:join(parent,"abc-workspace"),LOCK_MODE:"exnb",HOLD_MS:"20"},stdio:["ignore","pipe","pipe"]});
const [code]=await once(child,"close"); expect(code).toBe(1); await shared.close();
const child2=spawn(process.execPath,[join(process.cwd(),"test/fixtures/workspace-session-readers-worker.mjs")],{cwd:process.cwd(),env:{...process.env,WORKSPACE_ROOT:join(parent,"abc-workspace"),LOCK_MODE:"exnb",HOLD_MS:"5"},stdio:["ignore","pipe","pipe"]}); const [code2]=await once(child2,"close"); expect(code2).toBe(0);
});
it("permits coexisting production shared acquisitions and invalidates the source after transfer", async () => {
const parent=mkdtempSync(join(process.cwd(),"thoth-readers-")); roots.push(parent); const f=factory(parent); const source=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const second=await f.acquireOrProvision(f.canonicalInput("abc-workspace")); const a=await source.acquireSessionReadersShared(); const b=await second.acquireSessionReadersShared(); expect(a.rootIdentity.inode).toBe(b.rootIdentity.inode); await expect(source.close()).resolves.toBeUndefined(); await a.close(); await b.close();
});
});
+9 -2
View File
@@ -19,9 +19,9 @@ RUN npm ci --omit=dev \
FROM node:22-bookworm@sha256:7725a5c2c83eed1d36258c66efae14b1ceccd021db9ed1d9559d3335ed3d68ed AS backend-native-build
WORKDIR /src/backend
COPY backend/package*.json ./
RUN npm ci
COPY backend/native ./native
COPY backend/scripts ./scripts
COPY backend/native ./native
RUN npm ci
RUN npm run build:native
# ---- Stage 2: backend TypeScript -> dist ----
FROM node:22-bookworm@sha256:7725a5c2c83eed1d36258c66efae14b1ceccd021db9ed1d9559d3335ed3d68ed AS backend-build
@@ -29,10 +29,13 @@ WORKDIR /src/backend
COPY backend/package*.json ./
RUN npm ci --ignore-scripts
COPY backend/src ./src
COPY backend/scripts ./scripts
COPY backend/tsconfig*.json ./
RUN npm run build
RUN npm prune --omit=dev
COPY --from=backend-native-build /src/backend/native/workspace-fs-at/build/Release/workspace_fs_at.node ./native/workspace-fs-at/build/Release/workspace_fs_at.node
# fs-ext is the pinned runtime flock binding; carry its Node-22 build from the compiler stage.
COPY --from=backend-native-build /src/backend/node_modules/fs-ext/build ./node_modules/fs-ext/build
# ---- Stage 2: runtime (Python 3.12 nativo + Node 22 copiato, stesso glibc bookworm) ----
FROM python:3.12-slim-bookworm@sha256:d50fb7611f86d04a3b0471b46d7557818d88983fc3136726336b2a4c657aa30b AS runtime
@@ -85,6 +88,7 @@ RUN ln -s /opt/venv /app/harness/.venv
# Backend: dist + node_modules (stesso Node major 22 + glibc bookworm → compatibili)
COPY --from=backend-build /src/backend/dist /app/backend/dist
COPY --from=backend-build /src/backend/node_modules /app/backend/node_modules
COPY --from=backend-build /src/backend/native/workspace-fs-at/build/Release/workspace_fs_at.node /app/backend/native/workspace-fs-at/build/Release/workspace_fs_at.node
COPY backend/package*.json /app/backend/
# Runtime Pi is installed only from the committed lockfile. The image exposes its immutable
@@ -109,6 +113,9 @@ RUN /usr/local/bin/verify-line-endings /app/docker \
WORKDIR /app/backend
USER thoth
# Compiler-free Node 22 runtime smoke: load the repo addon, fsync an anchored directory, and load fs-ext.
RUN node --version | grep -Eq "^v22\." \
&& node -e 'const a=require("/app/backend/native/workspace-fs-at/build/Release/workspace_fs_at.node"); const r=a.openat({parent:null,name:"/",kind:"directory",createMode:0}); a.fsyncDirectory(r.handle); a.close(r.handle); require("fs-ext")'
EXPOSE 8787
HEALTHCHECK --interval=15s --timeout=3s --retries=5 --start-period=30s \
CMD curl -fsS http://127.0.0.1:8787/health || exit 1