lib/personal-provider-install.js
Rama main · réplica de lectura del repositorio original.
// Exact human-settings review, one-use receipts, and scoped installer lifecycle.
var crypto = require('crypto');
var locks = new Map();
function attach(ctx, personal) {
var pending = new WeakMap(), running = new Map();
function emit(ws, request, state, extra) {
if (ws.readyState === 1) ctx.sendTo(ws, Object.assign({type:'personal_provider_install',vendor:request.vendor,personalRequestId:request.personalRequestId,state:state,requestId:request.requestId},extra || {}));
}
function valid(item) { return item.authority.validate() && Date.now() < item.expires; }
function release(ws) {
pending.delete(ws);
var item = running.get(ws); if (item) stop(ws,item);
}
async function stop(ws,item) {
if (item.stopping) return;
item.stopping = true; clearInterval(item.timer);
emit(ws,item.request,'stopping',{message:'Stopping the installation terminal…'});
try {
await ctx.tm.closeAndWait(item.terminal.id,{identity:item.terminal.identity,revalidate:function () { if (running.get(ws) !== item) throw new Error('Installation terminal changed.'); }});
emit(ws,item.request,'stopped',{message:'Installation terminal exited. Partial files may remain. Refresh status before explicitly reviewing another attempt.'});
locks.delete(item.key); running.delete(ws);
} catch(error) {
// Keep the lock: an uncertain effect must never be replayed automatically.
emit(ws,item.request,'unknown',{message:'Terminal shutdown could not be confirmed. Ask the server operator to inspect it before trying again.'});
}
}
function handle(ws,msg) {
if (typeof msg.type !== 'string' || msg.type.indexOf('personal_provider_install_') !== 0) return false;
try {
if (msg.type === 'personal_provider_install_stop') {
var active = running.get(ws);
if (active && active.request.personalRequestId === msg.personalRequestId && active.request.vendor === msg.vendor) stop(ws,active);
return true;
}
if (msg.type === 'personal_provider_install_cancel') {
var review = pending.get(ws);
if (review && review.token === msg.reviewId && review.request.personalRequestId === msg.personalRequestId && review.request.vendor === msg.vendor) pending.delete(ws);
return true;
}
if (msg.type === 'personal_provider_install_review') {
if (running.has(ws)) throw new Error('An installation is already running or its result is uncertain.');
var authority = personal.resolveInstall(ws,msg);
var key = JSON.stringify([authority.runtime.uid,authority.runtime.home,msg.vendor]);
if (locks.has(key)) throw new Error('An installation for this runtime account is already running or needs inspection.');
var item = {authority:authority,key:key,request:{vendor:msg.vendor,personalRequestId:msg.personalRequestId,requestId:msg.requestId},token:crypto.randomUUID(),expires:Date.now()+120000};
pending.set(ws,item);
emit(ws,msg,'review',{reviewId:item.token,command:authority.recipe.command,source:authority.recipe.source,effects:authority.recipe.effects,runtime:authority.runtime,message:'Review this exact installation. Review expires in two minutes; leaving settings cancels it.'});
return true;
}
if (msg.type === 'personal_provider_install_start') {
var item = pending.get(ws); pending.delete(ws);
if (!item || item.token !== msg.reviewId || item.request.vendor !== msg.vendor || item.request.personalRequestId !== msg.personalRequestId || !valid(item) || locks.has(item.key)) throw new Error('Installation review expired, changed or was already used. Refresh status and review again.');
var fresh = personal.resolveInstall(ws,msg);
if (fresh.recipe !== item.authority.recipe || JSON.stringify(fresh.runtime) !== JSON.stringify(item.authority.runtime)) throw new Error('Installation target changed.');
item.request.requestId = msg.requestId;
locks.set(item.key,item); running.set(ws,item);
var terminal;
try {
terminal = ctx.tm.create(100,24,item.authority.runtime,ws,{kind:'provider-install',title:'Install '+msg.vendor,providerInstall:msg.vendor,
revalidateInstall:function () { if (!valid(item) || running.get(ws) !== item || locks.get(item.key) !== item) throw new Error('Installation permission or identity changed.'); },
onExit:function (session) {
if (item.stopping || running.get(ws) !== item) return;
clearInterval(item.timer); running.delete(ws); locks.delete(item.key);
if (!valid(item)) { emit(ws,item.request,'unknown',{message:'Installation terminal exited after account context changed. Refresh status; no task was replayed.'}); return; }
var installed = null;
try { installed = !!require('./yoke/vendor-registry').getVendorInfo(msg.vendor).resolveExecutable({user:item.authority.identity.linuxUser}); } catch(error) {}
if (!valid(item)) { emit(ws,item.request,'unknown',{message:'Installation discovery ended after account context changed. Refresh status before continuing.'}); return; }
emit(ws,item.request,session.exitCode === 0 ? 'completed' : 'failed',{installed:installed,exitCode:session.exitCode,message:session.exitCode === 0 ? (installed === null ? 'Installer exited successfully, but runtime discovery is unavailable. Refresh status to check the installation.' : installed ? 'Installer exited successfully; runtime discovery now finds this provider. Authentication remains unknown.' : 'Installer exited successfully, but runtime discovery has not found the provider. Check ~/.local/bin on the runtime PATH and refresh status.') : 'Installer did not report success. Refresh status and inspect the runtime setup before explicitly reviewing another attempt.'});
}});
if (!terminal) throw new Error('The installation terminal could not be created.');
item.terminal = terminal;
// Review expiry bounds launch, not a running install. Authority stays live.
item.expires = Infinity;
item.timer = setInterval(function () { if (!valid(item)) stop(ws,item); },250);
item.timer.unref();
emit(ws,msg,'started',{message:'Installation started in the reviewed runtime account. Waiting for terminal exit; downloads may be quiet.'});
} catch(error) { emit(ws,item.request,'unknown',{message:'Installation launch could not be confirmed. Ask the server operator to inspect the runtime before retrying.'}); }
return true;
}
throw new Error('Unsupported installation operation.');
} catch(error) { emit(ws,msg,'refused',{message:error.message}); return true; }
}
return {handle:handle,release:release,destroy:function () { pending = new WeakMap(); running.forEach(function(item,ws){stop(ws,item);}); }};
}
module.exports = {attach:attach};