remote.ts 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /**
  2. * Shared remote-control helpers for the E2B subprocess adapter: SDK option
  3. * shaping, poll ticks, and the one tolerant process-group signal used by both
  4. * the ordinary-process and terminal teardown ladders.
  5. */
  6. import { CommandExitError, e2bControlEnvs, SandboxNotFoundError } from '@deepseek-ai/dsh-e2b'
  7. import type { Sandbox } from '@deepseek-ai/dsh-e2b'
  8. /**
  9. * Normalize an unknown rejection into an Error.
  10. * @param error - Any thrown or rejected value.
  11. * @returns The value itself when already an Error, else a stringified wrapper.
  12. */
  13. export function asError(error: unknown): Error {
  14. return error instanceof Error ? error : new Error(String(error))
  15. }
  16. /**
  17. * Shape the optional-signal SDK options object.
  18. * @param signal - Optional cancellation for one SDK request.
  19. * @returns An options fragment that omits an undefined signal.
  20. */
  21. export function signalOpts(signal: AbortSignal | undefined): { signal?: AbortSignal } {
  22. return signal === undefined ? {} : { signal }
  23. }
  24. /**
  25. * Shape control-shell command options with the isolated HOME override.
  26. * @param envs - Explicit environment entries for the control command.
  27. * @param signal - Optional cancellation for the SDK request.
  28. * @returns Options for `sandbox.commands.run` control invocations.
  29. */
  30. export function commandOpts(
  31. envs: Record<string, string>,
  32. signal?: AbortSignal,
  33. ): { envs: Record<string, string>; signal?: AbortSignal } {
  34. return { envs: e2bControlEnvs(envs), ...signalOpts(signal) }
  35. }
  36. /**
  37. * Resolve after one duration.
  38. * @param ms - Milliseconds to wait.
  39. * @returns Settles after the timeout.
  40. */
  41. export function delay(ms: number): Promise<void> {
  42. return new Promise(resolve => setTimeout(resolve, ms))
  43. }
  44. /**
  45. * Wait one poll interval or until the signal aborts.
  46. * @param pollMs - Poll cadence in milliseconds.
  47. * @param signal - Optional abort that ends the wait early.
  48. * @returns `true` after a full tick, `false` when aborted first.
  49. */
  50. export function waitTick(pollMs: number, signal?: AbortSignal): Promise<boolean> {
  51. if (signal?.aborted === true) return Promise.resolve(false)
  52. return new Promise<boolean>((resolve) => {
  53. const timer = setTimeout(() => {
  54. signal?.removeEventListener('abort', onAbort)
  55. resolve(true)
  56. }, pollMs)
  57. const onAbort = (): void => {
  58. clearTimeout(timer)
  59. resolve(false)
  60. }
  61. signal?.addEventListener('abort', onAbort, { once: true })
  62. })
  63. }
  64. /**
  65. * Signal remote process groups, tolerating the shared teardown outcomes: a
  66. * nonzero `kill` (groups already gone) and a disappeared sandbox. Both the
  67. * pgid-keyed process ladder and the sid-keyed terminal ladder deliver signals
  68. * through this single tolerance so they cannot drift apart.
  69. * @param sandbox - Live SDK handle.
  70. * @param envs - Control-shell environment entries.
  71. * @param groups - Positive process-group ids to signal.
  72. * @param signal - `TERM` or `KILL`.
  73. */
  74. export async function signalRemoteGroups(
  75. sandbox: Sandbox,
  76. envs: Record<string, string>,
  77. groups: readonly number[],
  78. signal: 'TERM' | 'KILL',
  79. ): Promise<void> {
  80. // TODO(e2b-pgid-identity): Prefer an atomic identity-bound group signal if E2B adds one;
  81. // a userspace identity precheck cannot close the numeric-PGID reuse race.
  82. try {
  83. await sandbox.commands.run(
  84. `kill -${signal} -- ${groups.map(group => `-${group}`).join(' ')}`,
  85. commandOpts(envs),
  86. )
  87. } catch (error: unknown) {
  88. if (!(error instanceof CommandExitError) && !(error instanceof SandboxNotFoundError)) throw error
  89. }
  90. }