Skip to content

Commit 6e64cad

Browse files
Merge 05b85e7 into 9710e56
2 parents 9710e56 + 05b85e7 commit 6e64cad

4 files changed

Lines changed: 173 additions & 279 deletions

File tree

packages/cli/src/commands/sandbox/exec.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -197,22 +197,15 @@ async function sendStdin(sandbox: Sandbox, pid: number): Promise<void> {
197197
return
198198
}
199199

200-
// Fail fast instead of leaving a command blocked on stdin forever.
201-
await killProcessBestEffort(sandbox, pid)
200+
// Fail fast, and avoid leaking a process blocked on stdin.
201+
try {
202+
await sandbox.commands.kill(pid)
203+
} catch (killErr) {
204+
console.error(
205+
'e2b: Failed to kill remote process after stdin EOF signaling failed.'
206+
)
207+
console.error(killErr)
208+
}
202209
throw err
203210
}
204211
}
205-
206-
async function killProcessBestEffort(
207-
sandbox: Sandbox,
208-
pid: number
209-
): Promise<void> {
210-
try {
211-
await sandbox.commands.kill(pid)
212-
} catch (killErr) {
213-
console.error(
214-
'e2b: Failed to kill remote process after stdin EOF signaling failed.'
215-
)
216-
console.error(killErr)
217-
}
218-
}

packages/cli/tests/commands/sandbox/backend_integration.test.ts

Lines changed: 41 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { spawn, spawnSync } from 'node:child_process'
2-
import path from 'node:path'
31
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
42
import { Sandbox } from 'e2b'
53
import { getUserConfig } from 'src/user'
4+
import {
5+
bufferToText,
6+
isDebug,
7+
parseEnvInt,
8+
runCli,
9+
runCliWithPipedStdin,
10+
} from '../../setup'
611

712
type UserConfigWithDomain = NonNullable<ReturnType<typeof getUserConfig>> & {
813
domain?: string
@@ -16,21 +21,30 @@ const domain =
1621
userConfig?.domain ||
1722
'e2b.app'
1823
const apiKey = process.env.E2B_API_KEY || userConfig?.teamApiKey
24+
const shouldSkip = !apiKey || isDebug
25+
const integrationTest = test.skipIf(shouldSkip)
1926
const templateId =
2027
process.env.E2B_CLI_BACKEND_TEMPLATE_ID ||
2128
process.env.E2B_TEMPLATE_ID ||
2229
'base'
23-
const isDebug = process.env.E2B_DEBUG !== undefined
24-
const hasCreds = Boolean(apiKey)
25-
const shouldSkip = !hasCreds || isDebug
26-
const testIf = test.skipIf(shouldSkip)
27-
const cliPath = path.join(process.cwd(), 'dist', 'index.js')
2830
const sandboxTimeoutMs = parseEnvInt(
2931
'E2B_CLI_BACKEND_SANDBOX_TIMEOUT_MS',
3032
20_000
3133
)
3234
const perTestTimeoutMs = parseEnvInt('E2B_CLI_BACKEND_TEST_TIMEOUT_MS', 30_000)
3335
const spawnTimeoutMs = perTestTimeoutMs
36+
const cliEnv: NodeJS.ProcessEnv = {
37+
...process.env,
38+
E2B_DOMAIN: domain,
39+
E2B_API_KEY: apiKey,
40+
}
41+
42+
delete cliEnv.E2B_DEBUG
43+
44+
const runCliInSandbox = (args: string[]) =>
45+
runCli(args, { timeoutMs: spawnTimeoutMs, env: cliEnv })
46+
const runCliWithPipeInSandbox = (args: string[], input: Buffer) =>
47+
runCliWithPipedStdin(args, input, { timeoutMs: spawnTimeoutMs, env: cliEnv })
3448

3549
describe('sandbox cli backend integration', () => {
3650
let sandbox: Sandbox
@@ -57,17 +71,21 @@ describe('sandbox cli backend integration', () => {
5771
}
5872
}, 15_000)
5973

60-
testIf('list shows the sandbox', { timeout: perTestTimeoutMs }, async () => {
61-
const listResult = runCli(['sandbox', 'list', '--format', 'json'])
62-
expect(listResult.status).toBe(0)
63-
expect(sandboxExistsInList(listResult.stdout, sandbox.sandboxId)).toBe(true)
64-
})
74+
integrationTest(
75+
'list shows the sandbox',
76+
{ timeout: perTestTimeoutMs },
77+
async () => {
78+
const listResult = runCliInSandbox(['sandbox', 'list', '--format', 'json'])
79+
expect(listResult.status).toBe(0)
80+
expect(sandboxExistsInList(listResult.stdout, sandbox.sandboxId)).toBe(true)
81+
}
82+
)
6583

66-
testIf(
84+
integrationTest(
6785
'info shows the sandbox details',
6886
{ timeout: perTestTimeoutMs },
6987
async () => {
70-
const infoResult = runCli([
88+
const infoResult = runCliInSandbox([
7189
'sandbox',
7290
'info',
7391
sandbox.sandboxId,
@@ -86,11 +104,11 @@ describe('sandbox cli backend integration', () => {
86104
}
87105
)
88106

89-
testIf(
107+
integrationTest(
90108
'exec runs a command without piped stdin',
91109
{ timeout: perTestTimeoutMs },
92110
async () => {
93-
const execResult = runCli([
111+
const execResult = runCliInSandbox([
94112
'sandbox',
95113
'exec',
96114
sandbox.sandboxId,
@@ -104,11 +122,11 @@ describe('sandbox cli backend integration', () => {
104122
}
105123
)
106124

107-
testIf(
125+
integrationTest(
108126
'exec runs a command with piped stdin',
109127
{ timeout: perTestTimeoutMs },
110128
async () => {
111-
const pipedExecResult = await runCliWithPipedStdin(
129+
const pipedExecResult = await runCliWithPipeInSandbox(
112130
['sandbox', 'exec', sandbox.sandboxId, '--', 'sh', '-lc', 'wc -c'],
113131
Buffer.from('hello\n', 'utf8')
114132
)
@@ -120,27 +138,11 @@ describe('sandbox cli backend integration', () => {
120138
}
121139
)
122140

123-
/** Note: removing this test for now because it can be slow to get the logs causing tests to time out */
124-
// testIf(
125-
// 'logs returns successfully',
126-
// { timeout: perTestTimeoutMs },
127-
// async () => {
128-
// const logsResult = runCli([
129-
// 'sandbox',
130-
// 'logs',
131-
// sandbox.sandboxId,
132-
// '--format',
133-
// 'json',
134-
// ])
135-
// expect(logsResult.status).toBe(0)
136-
// }
137-
// )
138-
139-
testIf(
141+
integrationTest(
140142
'metrics returns successfully',
141143
{ timeout: perTestTimeoutMs },
142144
async () => {
143-
const metricsResult = runCli([
145+
const metricsResult = runCliInSandbox([
144146
'sandbox',
145147
'metrics',
146148
sandbox.sandboxId,
@@ -151,102 +153,24 @@ describe('sandbox cli backend integration', () => {
151153
}
152154
)
153155

154-
testIf(
156+
integrationTest(
155157
'kill removes the sandbox',
156158
{ timeout: perTestTimeoutMs },
157159
async () => {
158-
const killResult = runCli(['sandbox', 'kill', sandbox.sandboxId])
160+
const killResult = runCliInSandbox(['sandbox', 'kill', sandbox.sandboxId])
159161
expect(killResult.status).toBe(0)
160162

161163
await assertSandboxNotListed(sandbox.sandboxId)
162164
}
163165
)
164166
})
165167

166-
function runCli(
167-
args: string[],
168-
opts?: { input?: string | Buffer }
169-
): ReturnType<typeof spawnSync> {
170-
const env: NodeJS.ProcessEnv = {
171-
...process.env,
172-
E2B_DOMAIN: domain,
173-
E2B_API_KEY: apiKey,
174-
}
175-
delete env.E2B_DEBUG
176-
177-
return spawnSync('node', [cliPath, ...args], {
178-
env,
179-
input: opts?.input,
180-
encoding: 'utf8',
181-
timeout: spawnTimeoutMs,
182-
})
183-
}
184-
185-
type PipeRunResult = {
186-
status: number | null
187-
stdout: Buffer
188-
stderr: Buffer
189-
error?: Error
190-
}
191-
192-
function runCliWithPipedStdin(
193-
args: string[],
194-
input: Buffer
195-
): Promise<PipeRunResult> {
196-
const env: NodeJS.ProcessEnv = {
197-
...process.env,
198-
E2B_DOMAIN: domain,
199-
E2B_API_KEY: apiKey,
200-
}
201-
delete env.E2B_DEBUG
202-
203-
return new Promise((resolve) => {
204-
const child = spawn('node', [cliPath, ...args], {
205-
env,
206-
stdio: ['pipe', 'pipe', 'pipe'],
207-
})
208-
209-
const stdoutChunks: Buffer[] = []
210-
const stderrChunks: Buffer[] = []
211-
let childError: Error | undefined
212-
let timedOut = false
213-
214-
const timer = setTimeout(() => {
215-
timedOut = true
216-
child.kill()
217-
}, spawnTimeoutMs)
218-
219-
child.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)))
220-
child.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)))
221-
child.on('error', (err) => {
222-
childError = err
223-
})
224-
child.on('close', (code) => {
225-
clearTimeout(timer)
226-
const timeoutError = timedOut
227-
? Object.assign(new Error('CLI command timed out'), {
228-
code: 'ETIMEDOUT',
229-
} as NodeJS.ErrnoException)
230-
: undefined
231-
resolve({
232-
status: code,
233-
stdout: Buffer.concat(stdoutChunks),
234-
stderr: Buffer.concat(stderrChunks),
235-
error: childError ?? timeoutError,
236-
})
237-
})
238-
239-
child.stdin.write(input)
240-
child.stdin.end()
241-
})
242-
}
243-
244168
async function assertSandboxNotListed(sandboxId: string): Promise<void> {
245169
const retries = 10
246170
const delayMs = 500
247171

248172
for (let i = 0; i < retries; i++) {
249-
const listResult = runCli(['sandbox', 'list', '--format', 'json'])
173+
const listResult = runCliInSandbox(['sandbox', 'list', '--format', 'json'])
250174
if (listResult.status === 0) {
251175
const exists = sandboxExistsInList(listResult.stdout, sandboxId)
252176
if (!exists) {
@@ -273,22 +197,6 @@ function sandboxExistsInList(
273197
return parsed.some((item) => item.sandboxId === sandboxId)
274198
}
275199

276-
function bufferToText(value: Buffer | string | null | undefined): string {
277-
if (!value) {
278-
return ''
279-
}
280-
return typeof value === 'string' ? value : value.toString('utf8')
281-
}
282-
283-
function parseEnvInt(name: string, fallback: number): number {
284-
const raw = process.env[name]
285-
if (!raw) {
286-
return fallback
287-
}
288-
const parsed = Number.parseInt(raw, 10)
289-
return Number.isFinite(parsed) ? parsed : fallback
290-
}
291-
292200
function safeGetUserConfig(): ReturnType<typeof getUserConfig> | null {
293201
try {
294202
return getUserConfig()

0 commit comments

Comments
 (0)