import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import {randomUUID} from 'node:crypto'; import {nativeProfileDirectories} from './native-preferences.mjs'; const leases=new Set(); export function nativeSessionWorkspaceRoot(){const root=path.join(path.dirname(nativeProfileDirectories().datafiles),'sessions');fs.mkdirSync(root,{recursive:true,mode:0o700});return fs.realpathSync(root);} const failure=(code,statusCode=409)=>Object.assign(new Error(code),{statusCode}); export function reserveNativeSessionWorkspace(request,checkUsage){ const key=request.desktop?.preserveLayout?request.desktop?.recoveryKey:undefined; if(key!==undefined&&(typeof key!=='string'||!/^[a-f0-9-]{36}$/.test(key)))throw failure('SESSION_RECOVERY_KEY_INVALID',400); const persistent=Boolean(key),workspace=persistent?path.join(nativeSessionWorkspaceRoot(),key):fs.mkdtempSync(path.join(os.tmpdir(),'blender-session-')); if(leases.has(workspace))throw failure('SESSION_RECOVERY_OWNER_ACTIVE'); leases.add(workspace); try{ if(fs.existsSync(workspace)){const stat=fs.lstatSync(workspace);if(!stat.isDirectory()||stat.isSymbolicLink())throw failure('SESSION_WORKSPACE_INVALID');}else fs.mkdirSync(workspace,{mode:0o700}); checkUsage(workspace); const marker=path.join(workspace,'.session-slot.json'),retained=persistent&&fs.existsSync(marker); if(retained){const stat=fs.lstatSync(marker);if(!stat.isFile()||stat.isSymbolicLink()||stat.size>4096)throw failure('SESSION_WORKSPACE_MARKER_INVALID');const saved=JSON.parse(fs.readFileSync(marker,'utf8'));if(saved.schemaVersion!==1||saved.key!==key)throw failure('SESSION_WORKSPACE_OWNER_INVALID');} return {workspace,persistent,retained,key,release:()=>leases.delete(workspace)}; }catch(error){leases.delete(workspace);if(!persistent)fs.rmSync(workspace,{recursive:true,force:true});throw error;} } export function prepareNativeSessionWorkspace(lease){ const {workspace}=lease,orphans=fs.readdirSync(workspace).filter(name=>name==='source.blend'||/^(result|resources|preferences|project)-[0-9]{8}\.(blend|zip)$/.test(name)); if(orphans.length){const retired=path.join(workspace,'.retired');if(fs.existsSync(retired)){const stat=fs.lstatSync(retired);if(!stat.isDirectory()||stat.isSymbolicLink())throw failure('SESSION_RETENTION_DIRECTORY_INVALID');}else fs.mkdirSync(retired,{mode:0o700});const archive=path.join(retired,randomUUID());fs.mkdirSync(archive,{mode:0o700});for(const name of orphans)fs.renameSync(path.join(workspace,name),path.join(archive,name));} for(const directory of ['commands','responses','events','runtime'])fs.rmSync(path.join(workspace,directory),{recursive:true,force:true}); for(const name of ['request.json','session-ready.json','desktop-state.json','input-ack.json','input-error.json','pen-input.json','pen-output.json','startup-ready.json','desktop-recovery.json','screen.png','screen-meta.json','screen.tmp','stop'])for(const file of [name,name+'.tmp'])fs.rmSync(path.join(workspace,file),{force:true}); } export function markNativeSessionWorkspace(lease){if(!lease.persistent)return;const target=path.join(lease.workspace,'.session-slot.json'),temporary=target+'.'+randomUUID()+'.tmp';fs.writeFileSync(temporary,JSON.stringify({schemaVersion:1,key:lease.key}),{flag:'wx',mode:0o600});fs.renameSync(temporary,target);}