클라우드 샌드박스에서 대화형 셸 실행
클라우드 샌드박스에서 대화형 셸 실행 (Run an interactive shell in a cloud sandbox)
입력을 기대하는 프로세스를 클라우드 샌드박스에서 터미널로 실행하는 방법을 알아볼게요.
출처: 문서
본문
대화형 입력을 기대하는 프로세스를 터미널과 함께 실행해요. 터미널 세션은 캡처된 명령과 달라요 — 입력, 출력, 연결 수명을 직접 관리하게 되죠.
첫 샌드박스에서 얻은 실행 중인 샌드박스 핸들을 사용해요. 명령은 ['sh'] 같은 인자 배열로 전달해요.
터미널 프로세스 시작하기 (Start a terminal process)
Pseudo-terminal과 초기 터미널 크기로 프로세스를 시작해요. 반환된 프로세스 핸들이 이 세션을 식별해요. 나중에 다시 연결하려면 그 리소스 이름을 저장해 두세요.
이 시작 요청에 멱등성 키(idempotency key) 하나를 사용해요. 다시 연결할 때 프로세스를 새로 시작할 필요는 없어요.
TypeScript
return sandbox.processes.start(
{ args, pty: { initialSize: { rows: 40, cols: 120 } } },
{ idempotencyKey: requestId },
);
전체 TypeScript 예시: shell/create.ts
import type { Sandbox } from '@docker/sandboxes';
export async function createProcess(
sandbox: Sandbox,
args: string[],
requestId: string,
) {
return sandbox.processes.start(
{ args, pty: { initialSize: { rows: 40, cols: 120 } } },
{ idempotencyKey: requestId },
);
}
입력 보내고 출력 읽기 (Send input and read output)
프로세스에 연결하고, 입력을 쓰고, 출력 이벤트를 소비해요. 이 예시는 유한한 입력 버퍼를 보내고 표준 입력을 닫아요. 터미널 애플리케이션은 사용자가 끝낼 때까지 입력을 열어 두어야 해요.
출력 청크를 여러분의 터미널이나 콜백으로 전달해요. exit 이벤트가 프로세스 종료 코드를 알려줘요. 그 이벤트 없이 연결이 끝났다면 명령이 끝났다고 단정할 수 없어요.
소비를 멈추면 연결을 닫아요. 연결을 닫는 것과 프로세스를 종료하는 것은 별개의 동작이에요.
TypeScript
const connection = await process.connect();
try {
await connection.write(stdin);
await connection.closeStdin();
for await (const event of connection) {
if (event.type === 'chunk')
write(
event.data ?? new Uint8Array(),
BigInt(event.streamSequence ?? '0'),
);
if (event.type === 'exited') return event.exitCode ?? 0;
}
throw new Error(
`Stream for ${process.name} ended before the process exited`,
);
} finally {
await connection.close();
}
전체 TypeScript 예시: shell/attach.ts
import type { Process } from '@docker/sandboxes';
export async function attachToProcess(
process: Process,
stdin: Uint8Array,
write: (bytes: Uint8Array, sequence: bigint) => void,
) {
const connection = await process.connect();
try {
await connection.write(stdin);
await connection.closeStdin();
for await (const event of connection) {
if (event.type === 'chunk')
write(
event.data ?? new Uint8Array(),
BigInt(event.streamSequence ?? '0'),
);
if (event.type === 'exited') return event.exitCode ?? 0;
}
throw new Error(
`Stream for ${process.name} ended before the process exited`,
);
} finally {
await connection.close();
}
}
연결 끊김 후 출력 이어받기 (Resume output after a disconnect)
각 출력 청크를 처리한 뒤 시퀀스 번호를 기록해요. 다시 연결할 때 마지막으로 처리한 시퀀스 번호를 전달해 그 지점 이후부터 이어받아요.
TypeScript 프로세스 핸들로 열린 연결은 출력을 소비하는 동안 일시적 연결 끊김 이후 자동으로 이어져요. 이때 여러분의 애플리케이션에 마지막으로 전달된 청크를 기준으로 하지, 애플리케이션이 다른 곳에 저장한 마지막 청크를 기준으로 하지 않아요. Raw 스트림은 명시적으로 다시 연결해야 해요. 애플리케이션을 재시작한 뒤 이어받으려면 자신만의 커서(cursor)를 유지하세요. 입력은 절대 재생되지 않아요. 쓰기가 실패하면 그 입력을 다시 보내기 전에 프로세스를 확인하세요.
커서와 함께 프로세스 이름을 보관해요. 한 프로세스의 커서로 다른 프로세스의 출력을 식별할 수 없어요. 애플리케이션이 해당 출력을 처리한 뒤에만 커서를 유지(persist)하세요.
TypeScript
const connection = await process.connect({ resumeFrom });
let lastSequence = BigInt(resumeFrom);
try {
for await (const event of connection) {
if (event.type === 'chunk') {
write(
event.data ?? new Uint8Array(),
(lastSequence = BigInt(event.streamSequence ?? '0')),
);
}
}
return lastSequence;
} finally {
await connection.close();
}
전체 TypeScript 예시: shell/resume.ts
import type { Process } from '@docker/sandboxes';
export async function resumeProcessOutput(
process: Process,
resumeFrom: bigint,
write: (bytes: Uint8Array, sequence: bigint) => void,
) {
const connection = await process.connect({ resumeFrom });
let lastSequence = BigInt(resumeFrom);
try {
for await (const event of connection) {
if (event.type === 'chunk') {
write(
event.data ?? new Uint8Array(),
(lastSequence = BigInt(event.streamSequence ?? '0')),
);
}
}
}
return lastSequence;
} finally {
await connection.close();
}
시그널 보내기 (Send a signal)
이름으로 프로세스를 가져와 애플리케이션이 보내려는 시그널을 전송해요. 프로세스가 자신의 파일을 정리해야 할 때는 정상 종료(graceful termination) 시그널을 사용해요.
시그널은 프로세스에 작용하지, 샌드박스를 삭제하지 않아요. 모든 작업이 끝나면 샌드박스는 별도로 삭제해 주세요.
TypeScript
const process = await sandbox.processes.get(name);
await process.signal(signal);
전체 TypeScript 예시: shell/signal.ts
import type { Sandbox, Process } from '@docker/sandboxes';
export async function signalProcess(
sandbox: Sandbox,
name: string,
signal: Parameters<Process['signal']>[0],
) {
const process = await sandbox.processes.get(name);
await process.signal(signal);
}
더 알아보기 (Learn more)
전체 레시피 목록은 Browse all recipes 에서 확인할 수 있어요.