import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import crypto from 'node:crypto'; import {fileURLToPath} from 'node:url'; import {startBoundedServerJobProcess,startServerJobProcess,cancelServerJobProcess} from './server-job-process.mjs'; import {createResourceBundle,materializeResources,releaseResourceBundle,captureResourceBundle,exportResourceBundle,describeResourceBundle} from './native-resources.mjs'; import {handleNativeSession} from './native-session-server.mjs'; import {nativeSessionWorkspaceRoot} from './native-session-workspace.mjs'; import {prepareNativePreferences,nativeProfileDirectories} from './native-preferences.mjs'; const ROOT=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../..'); const jobs=new Map(); const MAX_SOURCE=512*1024*1024, MAX_ARTIFACTS=256*1024*1024; const hash=bytes=>crypto.createHash('sha256').update(bytes).digest('hex'); const canonical=value=>Array.isArray(value)?`[${value.map(canonical).join(',')}]`:value&&typeof value==='object'?`{${Object.keys(value).sort().map(k=>`${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`:JSON.stringify(value); const fail=(message,statusCode=400)=>{throw Object.assign(new Error(message),{statusCode,errorCode:message});}; const json=(res,status,data)=>{res.statusCode=status;res.setHeader('Content-Type','application/json');res.setHeader('Cache-Control','no-store');res.end(JSON.stringify(data));}; const executable=name=>process.env.PATH?.split(path.delimiter).map(dir=>path.join(dir,name)).find(p=>{try{fs.accessSync(p,fs.constants.X_OK);return true;}catch{return false;}}); const terminal=(entry,status,error)=>({schemaVersion:1,jobId:entry.request.jobId,kind:entry.request.kind,requestSha256:entry.request.requestSha256,sourceBlendSha256:entry.request.sourceBlendSha256,sourceRevision:entry.request.sourceRevision,status,...(error?{error,...(/^([A-Z][A-Z0-9_.-]*)(?::|$)/u.exec(error)?.[1]?{errorCode:/^([A-Z][A-Z0-9_.-]*)(?::|$)/u.exec(error)?.[1]}:{})}: {})}); function cleanup(entry){if(entry.workspace){fs.rmSync(entry.workspace,{recursive:true,force:true});entry.workspace=null;}} function workspaceUsage(directory,state={bytes:0,files:0}){ for(const entry of fs.readdirSync(directory,{withFileTypes:true})){ if(++state.files>10000)throw new Error('NATIVE_WORKSPACE_FILE_BUDGET'); const file=path.join(directory,entry.name);let info; try{info=fs.lstatSync(file);}catch(error){if(error.code==='ENOENT')continue;throw error;} if(info.isDirectory())workspaceUsage(file,state); else if(info.isFile())state.bytes+=info.size; if(state.bytes>1024*1024*1024)throw new Error('NATIVE_WORKSPACE_BYTE_BUDGET'); } return state; } function collect(req,limit){return new Promise((resolve,reject)=>{const chunks=[];let length=0;req.on('data',chunk=>{length+=chunk.length;if(length>limit){reject(new Error('NATIVE_REQUEST_TOO_LARGE'));req.destroy();}else chunks.push(chunk);});req.on('end',()=>resolve(Buffer.concat(chunks)));req.on('error',reject);});} function integer(value,min,max){return Number.isSafeInteger(value)&&value>=min&&value<=max;} function blenderBinary(options){ const configured=options.blenderBinary??process.env.BLENDER_BIN; if(configured&&configured!=='blender')return configured; const adapter=path.join(ROOT,'build_blender_native/desktop-adapter/bin/blender'); if(fs.existsSync(adapter))return adapter; const local=path.join(ROOT,'build_blender_native/bin/blender'); return fs.existsSync(local)?local:executable('blender')??'blender'; } function boundedString(value,max){return typeof value==='string'&&Buffer.byteLength(value)<=max&&!value.includes('\0');} function validate(request){ if(request?.schemaVersion!==1||!['RENDER','PYTHON','DESKTOP_OPERATOR','EXPORT'].includes(request.kind))fail('NATIVE_REQUEST_KIND'); if(!/^[a-zA-Z0-9_-]{1,128}$/.test(request.jobId??'')||!integer(request.sourceRevision,0,Number.MAX_SAFE_INTEGER)||!integer(request.sourceBlendByteLength,1,MAX_SOURCE)||!integer(request.timeoutMs,1000,600000))fail('NATIVE_REQUEST_BUDGET'); for(const key of ['sourceBlendSha256','requestSha256'])if(!/^[a-f0-9]{64}$/.test(request[key]??''))fail('NATIVE_REQUEST_HASH'); const {requestSha256,...unsigned}=request;if(hash(Buffer.from(canonical(unsigned)))!==requestSha256)fail('NATIVE_REQUEST_BINDING'); if(request.resources&&(!/^[a-f0-9]{64}$/.test(request.resources.token??'')||!/^[a-f0-9]{64}$/.test(request.resources.sha256??'')))fail('NATIVE_RESOURCE_BINDING'); if(request.desktop&&(!integer(request.desktop.width,640,2560)||!integer(request.desktop.height,360,1600)||typeof request.desktop.preserveLayout!=='boolean'||request.desktop.useSavedStartup!==undefined&&typeof request.desktop.useSavedStartup!=='boolean'))fail('NATIVE_DESKTOP_SIZE'); if(request.desktop?.recoveryKey!==undefined&&!/^[a-f0-9-]{36}$/.test(request.desktop.recoveryKey))fail('NATIVE_RECOVERY_KEY'); if(request.desktop?.recoverySavedAt!==undefined&&!integer(request.desktop.recoverySavedAt,0,Number.MAX_SAFE_INTEGER))fail('NATIVE_RECOVERY_TIMESTAMP'); const context=request.context;if(!context||typeof context!=='object'||Array.isArray(context))fail('NATIVE_CONTEXT_INVALID'); for(const key of ['sceneName','activeObject','nodeTreeName'])if(context[key]!==undefined&&!boundedString(context[key],256))fail('NATIVE_CONTEXT_NAME'); if(context.nodeTreeType!==undefined&&!['ShaderNodeTree','GeometryNodeTree','CompositorNodeTree'].includes(context.nodeTreeType))fail('NATIVE_NODE_EDITOR_TYPE'); if(context.selectedObjects!==undefined&&(!Array.isArray(context.selectedObjects)||context.selectedObjects.length>4096||context.selectedObjects.some(v=>!boundedString(v,256))))fail('NATIVE_SELECTION_INVALID'); if(context.mode!==undefined&&!['OBJECT','EDIT','POSE','SCULPT'].includes(context.mode))fail('NATIVE_MODE_INVALID'); if(context.meshSelection!==undefined){const s=context.meshSelection;if(!s||!['VERT','EDGE','FACE'].includes(s.domain)||!Array.isArray(s.indices)||s.indices.length>50000||s.indices.some(i=>!integer(i,0,100000000)))fail('NATIVE_MESH_SELECTION_INVALID');} if(context.areaType!==undefined&&!['VIEW_3D','IMAGE_EDITOR','NODE_EDITOR','GRAPH_EDITOR','DOPESHEET_EDITOR','SEQUENCE_EDITOR','TEXT_EDITOR','NLA_EDITOR','CLIP_EDITOR','CONSOLE','INFO','OUTLINER','PROPERTIES','FILE_BROWSER','SPREADSHEET','PREFERENCES'].includes(context.areaType))fail('NATIVE_AREA_INVALID'); if(request.kind==='RENDER'){ const r=request.render; if(!r||!integer(r.frameStart,-1000000,1000000)||!integer(r.frameEnd,r.frameStart,1000000)||!integer(r.step,1,1000)||Math.floor((r.frameEnd-r.frameStart)/r.step)+1>240||!['PNG','OPEN_EXR','FFMPEG'].includes(r.format))fail('NATIVE_RENDER_RANGE'); if(r.engine!==undefined&&!['SCENE','CYCLES','BLENDER_EEVEE','BLENDER_WORKBENCH'].includes(r.engine))fail('NATIVE_RENDER_ENGINE'); if(r.resolutionPercentage!==undefined&&!integer(r.resolutionPercentage,1,100))fail('NATIVE_RENDER_RESOLUTION'); }else if(request.kind==='PYTHON'){ if(!request.python||!boundedString(request.python.source,65536)||!boundedString(request.python.textName,128)||!request.python.textName||typeof request.python.console!=='boolean')fail('NATIVE_PYTHON_INVALID'); }else if(request.kind==='EXPORT'){ const spec=request.export; if(!spec||!['GLB','GLTF','FBX','OBJ','STL','PLY','USD','ALEMBIC'].includes(spec.format)||typeof spec.selectedOnly!=='boolean')fail('NATIVE_EXPORT_FORMAT'); const scalar=v=>typeof v==='boolean'||typeof v==='number'&&Number.isFinite(v)||boundedString(v,4096); if(!spec.properties||typeof spec.properties!=='object'||Array.isArray(spec.properties)||Object.keys(spec.properties).length>128||Object.keys(spec.properties).some(key=>['filepath','directory','filename','files','check_existing','filter_glob','batch_mode','use_batch_own_dir','as_background_job','export_format','export_texture_dir','export_animation','use_selection','export_selected_objects','selected_objects_only','selected'].includes(key)||/(path|directory|filename)/iu.test(key))||Object.entries(spec.properties).some(([key,value])=>!/^[A-Za-z][A-Za-z0-9_]{0,127}$/.test(key)||!(scalar(value)||Array.isArray(value)&&value.length<=64&&value.every(scalar)))||Buffer.byteLength(JSON.stringify(spec.properties))>65536)fail('NATIVE_EXPORT_PROPERTIES'); }else{ const op=request.operator; if(!op||!boundedString(op.id,128)||!/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/.test(op.id))fail('NATIVE_OPERATOR_INVALID'); if(/^(wm|window|screen|file)\./.test(op.id))fail('NATIVE_OPERATOR_LIFECYCLE_BLOCKED'); const props=op.properties; if(!props||typeof props!=='object'||Array.isArray(props)||Object.keys(props).length>128)fail('NATIVE_OPERATOR_PROPERTIES'); const scalar=v=>typeof v==='boolean'||typeof v==='number'&&Number.isFinite(v)||boundedString(v,4096); for(const [k,v] of Object.entries(props))if(!/^[A-Za-z][A-Za-z0-9_]{0,127}$/.test(k)||!(scalar(v)||Array.isArray(v)&&v.length<=64&&v.every(scalar)))fail('NATIVE_OPERATOR_PROPERTY'); } } function localRequest(req){ const address=req.socket.remoteAddress??''; const url=new URL(req.url??'/','http://'+req.headers.host); if(!['127.0.0.1','::1','::ffff:127.0.0.1'].includes(address)||!['localhost','127.0.0.1','[::1]'].includes(url.hostname))fail('NATIVE_LOCAL_SERVICE_ONLY',403); if(req.headers.origin&&new URL(req.headers.origin).host!==req.headers.host)fail('NATIVE_ORIGIN_REJECTED',403); if(req.headers['x-blender-workflow']!=='1')fail('NATIVE_EXPLICIT_REQUEST_REQUIRED',403); } async function finish(entry){ try{ await entry.process.completion; if(entry.result)return; if(entry.process.receipt.cancelRequested)throw new Error('NATIVE_JOB_TIMEOUT'); if(entry.process.child.exitCode!==0)throw new Error('NATIVE_PROCESS_FAILED: '+entry.stderr.slice(-4096)); const p=path.join(entry.workspace,'result.json'); if(fs.lstatSync(p).isSymbolicLink()||fs.statSync(p).size>2*1024*1024)throw new Error('NATIVE_RESULT_INVALID'); const result=JSON.parse(fs.readFileSync(p,'utf8'));if(result.status==='FAILED'&&typeof result.error==='string'){const code=/^([A-Z][A-Z0-9_.-]*)(?::|$)/u.exec(result.error)?.[1];if(code)result.errorCode=code;} for(const key of ['schemaVersion','jobId','kind','requestSha256','sourceBlendSha256','sourceRevision'])if(result[key]!==entry.request[key])throw new Error('NATIVE_RESULT_BINDING'); if(!['SUCCEEDED','FAILED'].includes(result.status))throw new Error('NATIVE_RESULT_STATUS'); if(result.status==='SUCCEEDED'){ let total=0;const ids=new Set(); if(!Array.isArray(result.artifacts)||result.artifacts.length>242)throw new Error('NATIVE_RESULT_ARTIFACTS'); for(const artifact of result.artifacts){ if(!/^[a-zA-Z0-9_.-]+$/.test(artifact.id)||artifact.id.startsWith('.')||ids.has(artifact.id))throw new Error('NATIVE_ARTIFACT_NAME');ids.add(artifact.id); const file=path.join(entry.workspace,artifact.id),stat=fs.lstatSync(file); if(!stat.isFile()||stat.isSymbolicLink()||stat.size<1||stat.size!==artifact.bytes||total+stat.size>MAX_ARTIFACTS)throw new Error('NATIVE_ARTIFACT_SIZE'); if(hash(fs.readFileSync(file))!==artifact.sha256)throw new Error('NATIVE_ARTIFACT_HASH');total+=stat.size; } if(['PYTHON','DESKTOP_OPERATOR'].includes(entry.request.kind)&&!ids.has('result.blend'))throw new Error('NATIVE_RESULT_BLEND_MISSING'); if(entry.request.kind==='EXPORT'&&!ids.has('export-package.zip'))throw new Error('NATIVE_EXPORT_EMPTY'); if(entry.request.kind==='RENDER'&&!result.artifacts.some(a=>a.id.startsWith('frame-')||a.mime==='video/mp4'))throw new Error('NATIVE_RENDER_EMPTY'); if(entry.request.kind!=='EXPORT')result.resources=captureResourceBundle(entry.workspace); } entry.result=result; if(result.status==='FAILED')try{cleanup(entry);}catch(error){entry.cleanupError=String(error?.message??error);console.error('NATIVE_WORKSPACE_CLEANUP_FAILED',entry.request.jobId,entry.cleanupError);} }catch(error){entry.result??=terminal(entry,'FAILED',String(error.message??error));try{cleanup(entry);}catch(cleanupError){entry.cleanupError=String(cleanupError?.message??cleanupError);console.error('NATIVE_WORKSPACE_CLEANUP_FAILED',entry.request.jobId,entry.cleanupError);}} finally{ clearInterval(entry.monitor); const expire=(retry=0)=>{if(jobs.get(entry.request.jobId)!==entry)return;try{cleanup(entry);jobs.delete(entry.request.jobId);}catch(error){entry.cleanupError=String(error?.message??error);console.error('NATIVE_WORKSPACE_CLEANUP_FAILED',entry.request.jobId,{retry,message:entry.cleanupError});if(retry<3){entry.expiry=setTimeout(()=>expire(retry+1),60000*2**retry);entry.expiry.unref();}}}; entry.expiry=setTimeout(expire,30*60*1000);entry.expiry.unref(); } } function launch(workspace,request,options,persistent=false,sessionWorkspace){ const sandbox=executable('bwrap');if(!sandbox)fail('NATIVE_SANDBOX_NOT_INSTALLED',503); const blender=blenderBinary(options); const gui=persistent||['DESKTOP_OPERATOR','EXPORT'].includes(request.kind)||request.kind==='RENDER'&&request.render.engine!=='CYCLES'; fs.mkdirSync(path.join(workspace,'runtime'),{mode:0o700}); const config=prepareNativePreferences(workspace); const profileDirectories=nativeProfileDirectories(); if(persistent&&request.desktop?.preserveLayout&&request.desktop.recoveryKey){ const recoveryRoot=path.join(profileDirectories.datafiles,'project-saves',request.desktop.recoveryKey),latest=path.join(recoveryRoot,'latest.json'); if(fs.existsSync(latest)){ const metadata=JSON.parse(fs.readFileSync(latest,'utf8')); if(metadata.savedAt>(request.desktop.recoverySavedAt??0)){ if(!/^[a-f0-9]{32}$/.test(metadata.directory)||!/^[a-zA-Z0-9_.-]+\.blend$/.test(metadata.main))fail('NATIVE_RECOVERY_METADATA'); const directory=path.join(recoveryRoot,metadata.directory),source=path.join(directory,metadata.main),data=fs.readFileSync(source); if(data.length>256*1024*1024||hash(data)!==metadata.sha256)fail('NATIVE_RECOVERY_SOURCE'); const resources=path.join(workspace,'resources');if(!sessionWorkspace?.retained){fs.rmSync(resources,{recursive:true,force:true});fs.mkdirSync(resources);} const files=[];let total=0; const restore=folder=>{for(const item of fs.readdirSync(folder,{withFileTypes:true})){ const absolute=path.join(folder,item.name);if(absolute===source)continue; const stat=fs.lstatSync(absolute);if(stat.isSymbolicLink())fail('NATIVE_RECOVERY_SYMLINK'); if(stat.isDirectory())restore(absolute); else if(stat.isFile()){ if((total+=stat.size)>256*1024*1024||files.length>=4096)fail('NATIVE_RECOVERY_RESOURCE_BUDGET'); const relative=path.relative(directory,absolute),destination=path.join(resources,relative),bytes=fs.readFileSync(absolute); fs.mkdirSync(path.dirname(destination),{recursive:true});fs.writeFileSync(destination,bytes); files.push({path:relative.split(path.sep).join('/'),bytes:bytes.length,sha256:hash(bytes)}); } }};if(!sessionWorkspace?.retained)restore(directory);fs.mkdirSync(path.join(resources,'user'),{recursive:true}); fs.writeFileSync(path.join(workspace,'source.blend'),data);if(!sessionWorkspace?.retained)fs.writeFileSync(path.join(workspace,'resources.json'),JSON.stringify({files})); fs.writeFileSync(path.join(workspace,'desktop-recovery.json'),JSON.stringify({savedAt:metadata.savedAt,sha256:metadata.sha256})); } } } const args=['--die-with-parent','--new-session','--unshare-all','--ro-bind','/','/','--tmpfs','/tmp','--dir','/tmp/.X11-unix','--tmpfs','/run/user','--proc','/proc','--dev','/dev','--tmpfs',nativeSessionWorkspaceRoot(),'--bind',workspace,workspace, '--unsetenv','WAYLAND_DISPLAY','--unsetenv','DISPLAY','--unsetenv','XAUTHORITY','--unsetenv','DBUS_SESSION_BUS_ADDRESS', '--setenv','XDG_RUNTIME_DIR',path.join(workspace,'runtime'), '--setenv','BLENDER_USER_CONFIG',config, '--setenv','HOME',workspace,'--setenv','TMPDIR',workspace,'--setenv','PYTHONNOUSERSITE','1','--setenv','LIBGL_ALWAYS_SOFTWARE','1', '--setenv','__EGL_VENDOR_LIBRARY_FILENAMES','/usr/share/glvnd/egl_vendor.d/50_mesa.json','--chdir',workspace]; for(const [name,directory] of Object.entries(profileDirectories)){ if(persistent)args.push('--bind',directory,directory); args.push('--setenv',`BLENDER_USER_${name.toUpperCase()}`,directory); } const prlimit=executable('prlimit');if(!prlimit)fail('NATIVE_RESOURCE_LIMITER_NOT_INSTALLED',503); args.push(prlimit,'--as=8589934592','--fsize=536870912',persistent?'--cpu=7200':'--cpu=600','--'); const width=persistent?request.desktop?.width??1280:1280,height=persistent?request.desktop?.height??900:900; if(gui){const xvfb=executable('xvfb-run');if(!xvfb)fail('NATIVE_VIRTUAL_DISPLAY_NOT_INSTALLED',503);args.push(xvfb,'-a',`--server-args=-screen 0 ${width}x${height}x24 -nolisten tcp`);} if(persistent)args.push('/usr/bin/python3',path.join(ROOT,'tools/web/native-desktop-host.py'),workspace); args.push(blender,'--factory-startup','--disable-autoexec'); if(persistent)args.push('--window-geometry','0','0',String(width),String(height)); if(!gui)args.push('--background'); args.push('--python-exit-code','1','--python',path.join(ROOT,persistent?'tools/web/native-session-blender.py':'tools/web/native-workflow-blender.py'),'--',workspace); return (persistent?startServerJobProcess:startBoundedServerJobProcess)(sandbox,args,{cwd:workspace,timeoutMs:request.timeoutMs,stdio:['ignore','pipe','pipe']}); } export async function handleNativeWorkflowRequest(req,res,options={}){ if(await handleNativeSession(req,res,options,{localRequest,collect,json,fail,validate,launch,workspaceUsage,canonical}))return true; const url=new URL(req.url??'/','http://localhost'); if(url.pathname==='/api/blender-resources'){ try{localRequest(req);if(req.method!=='POST')fail('RESOURCE_METHOD',405); if(req.headers['x-blender-resource-action']==='release'){releaseResourceBundle(JSON.parse((await collect(req,4096)).toString()));json(res,200,{status:'RELEASED'});} else if(req.headers['x-blender-resource-action']==='describe')json(res,200,describeResourceBundle(JSON.parse((await collect(req,4096)).toString()))); else if(req.headers['x-blender-resource-action']==='export'){ const data=exportResourceBundle(JSON.parse((await collect(req,4096)).toString())); res.statusCode=200;res.setHeader('Content-Type','application/octet-stream');res.setHeader('Cache-Control','no-store');res.end(data); } else json(res,200,createResourceBundle(await collect(req,256*1024*1024+2*1024*1024)));return true; }catch(error){if(error.statusCode===405)res.setHeader('Allow','POST');json(res,error.statusCode??400,{status:'FAILED',error:String(error.message??error),...(typeof error.errorCode==='string'?{errorCode:error.errorCode}:{})});return true;} } if(url.pathname!=='/api/blender-workflow')return false; const action=req.headers['x-blender-workflow-action']??'submit'; try{ localRequest(req); if(action==='describe'){ if(req.method!=='POST')fail('NATIVE_METHOD',405); const body=JSON.parse((await collect(req,4096)).toString()); if(!boundedString(body.operator,128)||!/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/.test(body.operator))fail('NATIVE_OPERATOR_INVALID'); const handle=startBoundedServerJobProcess(options.pythonBinary??'python3',[path.join(ROOT,'tools/web/server-operator-job.py')],{timeoutMs:120000,stdio:['pipe','pipe','pipe']}); let text='',overflow=false;handle.child.stdout.on('data',chunk=>{if(text.length+chunk.length>2*1024*1024){overflow=true;void cancelServerJobProcess(handle,async()=>{});}else text+=chunk;}); handle.child.stdin.on('error',()=>{});handle.child.stdin.end(JSON.stringify({action:'describe-context',operator:body.operator,blenderBinary:blenderBinary(options)})); await handle.completion; if(overflow||handle.child.exitCode!==0)fail('NATIVE_SCHEMA_FAILED',422); const schema=JSON.parse(text);if(schema.status==='FAILED')fail(schema.errorMessage??'NATIVE_SCHEMA_FAILED',422); json(res,200,schema);return true; } if(action==='submit'){ if(req.method!=='POST')fail('NATIVE_METHOD',405); const data=await collect(req,MAX_SOURCE+256*1024+12); if(data.length<12||data.subarray(0,4).toString()!=='BWF1')fail('NATIVE_ENVELOPE'); const metadataLength=data.readUInt32LE(4),sourceLength=data.readUInt32LE(8); if(metadataLength>256*1024||sourceLength>MAX_SOURCE||12+metadataLength+sourceLength!==data.length)fail('NATIVE_ENVELOPE_LENGTH'); const request=JSON.parse(data.subarray(12,12+metadataLength).toString());validate(request); const source=data.subarray(12+metadataLength); if(source.length!==request.sourceBlendByteLength||hash(source)!==request.sourceBlendSha256)fail('NATIVE_SOURCE_BINDING'); if(jobs.has(request.jobId))fail('NATIVE_DUPLICATE_JOB',409); if([...jobs.values()].filter(e=>!e.result).length>=2)fail('NATIVE_JOB_CAPACITY',429); const workspace=fs.mkdtempSync(path.join(os.tmpdir(),'blender-workflow-')); let processHandle; try{fs.writeFileSync(path.join(workspace,'request.json'),JSON.stringify(request),{mode:0o600});fs.writeFileSync(path.join(workspace,'source.blend'),source,{mode:0o600});materializeResources(request.resources,workspace);processHandle=launch(workspace,request,options);} catch(error){fs.rmSync(workspace,{recursive:true,force:true});throw error;} const entry={workspace,request,process:processHandle,result:null,stderr:''};jobs.set(request.jobId,entry); processHandle.child.stdout.on('data',chunk=>{entry.stderr=(entry.stderr+chunk.toString()).slice(-16384);}); entry.monitor=setInterval(()=>{ if(entry.result||!entry.workspace)return; try{workspaceUsage(entry.workspace);} catch(error){entry.result=terminal(entry,'FAILED',String(error.message));void cancelServerJobProcess(entry.process,()=>cleanup(entry),{graceMs:500});} },1000);entry.monitor.unref(); processHandle.child.stderr.on('data',chunk=>{entry.stderr=(entry.stderr+chunk.toString()).slice(-16384);}); processHandle.child.on('error',error=>{entry.stderr=String(error);}); void finish(entry);json(res,202,terminal(entry,'RUNNING'));return true; } const body=req.method==='POST'?JSON.parse((await collect(req,4096)).toString()):{}; const id=body.jobId??url.searchParams.get('jobId'),binding=body.requestSha256??url.searchParams.get('requestSha256'); const entry=jobs.get(id);if(!entry||entry.request.requestSha256!==binding)fail('NATIVE_JOB_NOT_FOUND',404); if(action==='cancel'||action==='release'){ if(req.method!=='POST')fail('NATIVE_METHOD',405); if(!entry.result){entry.result=terminal(entry,'CANCELLED');try{await cancelServerJobProcess(entry.process,()=>cleanup(entry),{graceMs:500});}catch(error){entry.result.error=`NATIVE_CANCEL_CLEANUP_FAILED: ${String(error.message??error)}`;entry.result.errorCode='NATIVE_CANCEL_CLEANUP_FAILED';}} if(action==='release'){cleanup(entry);clearTimeout(entry.expiry);jobs.delete(id);} json(res,200,entry.result);return true; } if(req.method!=='GET')fail('NATIVE_METHOD',405); if(action==='artifact'){ const artifact=entry.result?.status==='SUCCEEDED'&&entry.result.artifacts.find(a=>a.id===url.searchParams.get('artifact')); if(!artifact||!entry.workspace)fail('NATIVE_ARTIFACT_NOT_FOUND',404); const file=path.join(entry.workspace,artifact.id); res.setHeader('Content-Type',artifact.mime);res.setHeader('Cache-Control','no-store');res.setHeader('Content-Length',artifact.bytes);res.setHeader('X-Content-SHA256',artifact.sha256); res.setHeader('Content-Disposition',`attachment; filename="${artifact.id}"`); fs.createReadStream(file).on('error',()=>res.destroy()).pipe(res);return true; } if(action!=='read')fail('NATIVE_ACTION_INVALID'); const result=entry.result??terminal(entry,'RUNNING'); if(!entry.result&&entry.workspace){try{const file=path.join(entry.workspace,'progress.json');if(fs.statSync(file).size<4096)result.progress=JSON.parse(fs.readFileSync(file,'utf8'));}catch{}} json(res,200,result);return true; }catch(error){if(error.statusCode===405)res.setHeader('Allow',action==='read'||action==='artifact'?'GET':'POST');if(error.statusCode===429)res.setHeader('Retry-After','1');json(res,error.statusCode??(error instanceof SyntaxError?400:500),{status:'FAILED',error:String(error.message??error),...(typeof error.errorCode==='string'?{errorCode:error.errorCode}:{})});return true;} }