클라우드 샌드박스에서 대화형 셸 실행하기

클라우드 샌드박스에서 대화형 셸 실행하기

대화형 입력을 기대하는 프로세스를 터미널로 실행해요. 캡처된(내용을 모아두는) 명령과 달리 터미널 세션은 입력·출력·연결 수명을 직접 관리해야 해요.

출처: 문서

본문

첫 번째 샌드박스에서 얻은 실행 중인 샌드박스 핸들을 사용해요. 명령은 ['sh'] 같은 인자 배열로 전달해요.

터미널 프로세스 시작하기

가상 터미널(pseudo-terminal)과 초기 터미널 크기로 프로세스를 시작해요. 반환된 프로세스 핸들이 이 세션을 식별해요. 나중에 다시 연결하려면 리소스 이름을 저장해 두면 돼요. 이 시작 요청에는 멱등성 키(idempotency key)를 하나 사용해요. 다시 연결해도 새 프로세스를 시작할 필요는 없어요.

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 },
  );
}

입력 보내고 출력 읽기

프로세스에 연결해서 입력을 쓰고 출력 이벤트를 소비해요. 이 예시는 유한한 입력 버퍼를 보내고 표준 입력을 닫아요. 터미널 애플리케이션은 사용자가 끝낼 때까지 입력을 열어 두어야 해요. 출력 청크를 자신의 터미널이나 콜백으로 전달해요. exit(종료) 이벤트가 프로세스 종료 코드를 알려줘요. 그 이벤트 없이 연결이 끝났다면 명령이 끝났다는 뜻이 아니에요. 소비를 멈추면 연결을 닫아요. 연결을 닫는 것과 프로세스를 종료하는 것은 별개의 동작이에요.

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();
  }
}

연결 끊김 후 출력 재개하기

각 출력 청크를 처리한 뒤 시퀀스 번호를 기록해요. 다시 연결할 때 마지막으로 처리한 시퀀스 번호를 넘겨주면 그 지점 이후부터 재개해요. 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();
  }
}

시그널 보내기

이름으로 프로세스를 얻어 애플리케이션이 의도한 시그널을 보내요. 프로세스가 자신의 파일을 정리해야 할 때는 우아한 종료 시그널을 사용해요. 시그널은 프로세스에 작용하지, 샌드박스를 삭제하지 않아요. 모든 작업이 끝나면 샌드박스를 별도로 삭제해요.

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)