@cjhyy/code-shell-server 0.9.20 → 0.9.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -447,6 +447,7 @@ export function createPanelRuntime(options) {
447
447
  }, 60_000);
448
448
  reaper.unref();
449
449
  const preparing = new Set();
450
+ const preparingTools = new Set();
450
451
  async function dispatchToolJobs(grant, method, params) {
451
452
  if (!grant.app.permissions.includes("process") || !grant.app.permissions.includes("resources"))
452
453
  error(403, "原生后台任务需要 process 和 resources 权限。");
@@ -466,21 +467,37 @@ export function createPanelRuntime(options) {
466
467
  error(403, "你取消了后台工具执行。");
467
468
  if (!(await authorized(grant)))
468
469
  error(410, "面板授权已失效。");
469
- const job = await service.start(scope, {
470
- entry: { name: input.entry, sha256: entry.sha256 },
471
- input: input.input,
472
- recovery: input.recovery ?? "manual",
473
- requestKey: input.requestKey,
474
- });
475
- if (!jobOwners.has(job.id))
476
- jobOwners.set(job.id, { owner: grant.owner, scope });
477
- if (!(await authorized(grant))) {
478
- if (jobOwners.get(job.id)?.owner === grant.owner)
479
- await service.cancel(scope, job.id);
480
- error(410, "登录授权已撤销。");
470
+ const preparation = {
471
+ owner: grant.owner,
472
+ appId: scope.appId,
473
+ controller: new AbortController(),
474
+ };
475
+ preparingTools.add(preparation);
476
+ try {
477
+ const job = await service.start(scope, {
478
+ entry: { name: input.entry, sha256: entry.sha256 },
479
+ input: input.input,
480
+ recovery: input.recovery ?? "manual",
481
+ requestKey: input.requestKey,
482
+ }, preparation.controller.signal);
483
+ if (!jobOwners.has(job.id))
484
+ jobOwners.set(job.id, { owner: grant.owner, scope });
485
+ if (!(await authorized(grant))) {
486
+ if (jobOwners.get(job.id)?.owner === grant.owner)
487
+ await service.cancel(scope, job.id);
488
+ error(410, "登录授权已撤销。");
489
+ }
490
+ emitTaskEvent(await service.get(scope, job.id));
491
+ return job;
492
+ }
493
+ catch (cause) {
494
+ if (preparation.controller.signal.aborted)
495
+ error(410, "登录授权已撤销,输入准备已取消。");
496
+ throw cause;
497
+ }
498
+ finally {
499
+ preparingTools.delete(preparation);
481
500
  }
482
- emitTaskEvent(await service.get(scope, job.id));
483
- return job;
484
501
  }
485
502
  if (method === "tasks.list") {
486
503
  const offset = input.offset ?? 0, limit = input.limit ?? 50;
@@ -743,6 +760,9 @@ export function createPanelRuntime(options) {
743
760
  };
744
761
  }
745
762
  function cancelOwner(owner) {
763
+ for (const preparation of preparingTools)
764
+ if (preparation.owner === owner)
765
+ preparation.controller.abort();
746
766
  for (const preparation of preparing)
747
767
  if (preparation.owner === owner)
748
768
  preparation.cancelled = true;
@@ -1187,6 +1207,9 @@ export function createPanelRuntime(options) {
1187
1207
  cancelOwner,
1188
1208
  invalidate(appId) {
1189
1209
  generation++;
1210
+ for (const preparation of preparingTools)
1211
+ if (!appId || preparation.appId === appId)
1212
+ preparation.controller.abort();
1190
1213
  for (const grant of grants.values())
1191
1214
  if (!appId || grant.app.id === appId)
1192
1215
  remove(grant);
@@ -1199,6 +1222,8 @@ export function createPanelRuntime(options) {
1199
1222
  },
1200
1223
  async close() {
1201
1224
  closed = true;
1225
+ for (const preparation of preparingTools)
1226
+ preparation.controller.abort();
1202
1227
  clearInterval(reaper);
1203
1228
  for (const grant of grants.values())
1204
1229
  remove(grant);
@@ -92,7 +92,7 @@ export declare class PanelToolJobService {
92
92
  get(rawScope: ToolJobScope, id: string): Promise<ToolJob & {
93
93
  readOnly: boolean;
94
94
  }>;
95
- start(rawScope: ToolJobScope, request: ToolJobRequest): Promise<ToolJob>;
95
+ start(rawScope: ToolJobScope, request: ToolJobRequest, signal?: AbortSignal): Promise<ToolJob>;
96
96
  private schedule;
97
97
  private pump;
98
98
  private run;
@@ -208,7 +208,8 @@ export class PanelToolJobService {
208
208
  const job = this.lookup(scope, id);
209
209
  return { ...publicJob(job), readOnly: job.scope.revision !== scope.revision };
210
210
  }
211
- async start(rawScope, request) {
211
+ async start(rawScope, request, signal) {
212
+ signal?.throwIfAborted();
212
213
  const scope = scopeValue(rawScope);
213
214
  const entry = entryValue(request?.entry);
214
215
  if (!["manual", "retry"].includes(request.recovery))
@@ -232,6 +233,7 @@ export class PanelToolJobService {
232
233
  await this.authorize(scope);
233
234
  let done;
234
235
  const reservation = await this.exclusive(async () => {
236
+ signal?.throwIfAborted();
235
237
  if (this.stopping)
236
238
  throw new Error("tool job service is shutting down");
237
239
  if (request.requestKey) {
@@ -268,8 +270,14 @@ export class PanelToolJobService {
268
270
  if (reservation.existing)
269
271
  return publicJob(reservation.existing);
270
272
  const { id, controller } = reservation;
273
+ const abort = () => controller.abort(signal?.reason);
274
+ signal?.addEventListener("abort", abort, { once: true });
275
+ if (signal?.aborted)
276
+ abort();
271
277
  try {
278
+ controller.signal.throwIfAborted();
272
279
  const workDir = await this.storage.directory(id, true, true);
280
+ controller.signal.throwIfAborted();
273
281
  const prepared = this.options.prepareInput
274
282
  ? await this.options.prepareInput(scope, input, workDir, controller.signal)
275
283
  : input;
@@ -296,6 +304,19 @@ export class PanelToolJobService {
296
304
  [...this.jobs.values()].some((other) => sameScope(other.scope, scope) && other.requestKey === request.requestKey))
297
305
  throw new Error("tool job request key is already being prepared");
298
306
  await this.commit(job);
307
+ // Cancellation can arrive during the durable write. The serial fence
308
+ // prevents the scheduler from selecting this job before cancellation.
309
+ if (controller.signal.aborted) {
310
+ job.status = "cancelled";
311
+ job.completedAt = this.now();
312
+ job.error = {
313
+ code: "CANCELLED",
314
+ message: "Task was cancelled before execution.",
315
+ retryable: job.recovery === "retry",
316
+ };
317
+ await this.commit(job);
318
+ controller.signal.throwIfAborted();
319
+ }
299
320
  });
300
321
  this.schedule();
301
322
  return publicJob(job);
@@ -307,6 +328,7 @@ export class PanelToolJobService {
307
328
  throw error;
308
329
  }
309
330
  finally {
331
+ signal?.removeEventListener("abort", abort);
310
332
  this.preparing.delete(id);
311
333
  done();
312
334
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.20",
3
+ "version": "0.9.21",
4
4
  "description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -51,13 +51,13 @@
51
51
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
52
52
  },
53
53
  "dependencies": {
54
- "@cjhyy/code-shell-capability-coding": "0.9.20",
55
- "@cjhyy/code-shell-core": "0.9.20",
56
- "@cjhyy/code-shell-web": "0.9.20",
54
+ "@cjhyy/code-shell-capability-coding": "0.9.21",
55
+ "@cjhyy/code-shell-core": "0.9.21",
56
+ "@cjhyy/code-shell-web": "0.9.21",
57
57
  "@modelcontextprotocol/sdk": "^1.12.1",
58
58
  "@types/ws": "^8.18.1",
59
59
  "ws": "^8.21.0",
60
- "@cjhyy/code-shell-link": "0.9.20"
60
+ "@cjhyy/code-shell-link": "0.9.21"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=20.10"