Skip to content

Commit bfb5a12

Browse files
new setup approach
1 parent c446d01 commit bfb5a12

File tree

2 files changed

+207
-96
lines changed

2 files changed

+207
-96
lines changed

src/bin/commands/build.ts

Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { createClient, createConfig, type Client } from "@hey-api/client-fetch";
88
import ora from "ora";
99
import type * as yargs from "yargs";
1010

11-
import { VMTier, CodeSandbox, Sandbox, SetupProgress } from "../../";
11+
import { VMTier, CodeSandbox, Sandbox } from "../../";
1212

1313
import {
1414
sandboxFork,
@@ -201,69 +201,44 @@ export const buildCommand: yargs.CommandModule<
201201
session = await sandbox.connect();
202202

203203
const disposableStore = new DisposableStore();
204-
const handleProgress = async (progress: SetupProgress) => {
205-
let buffer: string[] = [];
206-
207-
if (
208-
progress.state === "IN_PROGRESS" &&
209-
progress.steps.length > 0
210-
) {
211-
const step = progress.steps[progress.currentStepIndex];
212-
if (!step) {
213-
return;
214-
}
215204

205+
const steps = await session.setup.getSteps();
206+
207+
for (const step of steps) {
208+
const buffer: string[] = [];
209+
210+
try {
216211
spinner.start(
217212
createSpinnerMessage(
218-
`Running setup ${progress.currentStepIndex + 1} / ${
219-
progress.steps.length
213+
`Running setup ${steps.indexOf(step) + 1} / ${
214+
steps.length
220215
} - ${step.name}...`,
221216
sandboxId
222217
)
223218
);
224219

225-
const shellId = step.shellId;
226-
227-
if (shellId) {
228-
const shell = await session.shells.open(shellId, {
229-
ptySize: {
230-
cols: process.stderr.columns,
231-
rows: process.stderr.rows,
232-
},
233-
});
220+
disposableStore.add(
221+
step.onOutput((output) => {
222+
buffer.push(output);
223+
})
224+
);
234225

235-
disposableStore.add(
236-
shell.onOutput((data) => {
237-
buffer.push(data);
238-
})
239-
);
240-
}
241-
} else if (progress.state === "STOPPED") {
242-
const step = progress.steps[progress.currentStepIndex];
243-
if (!step) {
244-
return;
245-
}
246-
247-
if (step.finishStatus === "FAILED") {
248-
spinner.fail(
249-
createSpinnerMessage(
250-
`Setup step failed: ${step.name}`,
251-
sandboxId
252-
)
253-
);
254-
console.log(buffer.join("\n"));
255-
throw new Error(`Setup step failed: ${step.name}`);
256-
}
257-
}
258-
};
226+
const output = await step.open();
259227

260-
const progress = await session.setup.getProgress();
261-
await handleProgress(progress);
262-
disposableStore.add(
263-
session.setup.onSetupProgressUpdate(handleProgress)
264-
);
228+
buffer.push(...output.split("\n"));
265229

266-
await session.setup.waitForFinish();
230+
await step.waitForFinish();
231+
} catch (error) {
232+
spinner.fail(
233+
createSpinnerMessage(
234+
`Setup step failed: ${step.name}`,
235+
sandboxId
236+
)
237+
);
238+
console.log(buffer.join("\n"));
239+
throw new Error(`Setup step failed: ${step.name}`);
240+
}
241+
}
267242

268243
disposableStore.dispose();
269244

@@ -342,9 +317,9 @@ export const buildCommand: yargs.CommandModule<
342317
vm_ids: sandboxIds,
343318
},
344319
}),
345-
"Failed to create tag"
320+
"Failed to create template"
346321
);
347-
console.log("Tag created: " + data.tag_id);
322+
console.log("Template created: " + data.tag_id);
348323
} catch (error) {
349324
console.error(error);
350325
process.exit(1);
Lines changed: 177 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,209 @@
1-
import type { Id, IPitcherClient } from "@codesandbox/pitcher-client";
2-
import { listenOnce } from "@codesandbox/pitcher-common/dist/event";
1+
import {
2+
Barrier,
3+
type IPitcherClient,
4+
type protocol,
5+
} from "@codesandbox/pitcher-client";
36

47
import { Disposable } from "../../utils/disposable";
58
import { Emitter } from "../../utils/event";
9+
import { DEFAULT_SHELL_SIZE } from "./terminals";
610

711
export class Setup {
812
private disposable = new Disposable();
9-
private readonly onSetupProgressUpdateEmitter = this.disposable.addDisposable(
10-
new Emitter<SetupProgress>()
13+
private steps: Promise<Step[]>;
14+
private setupProgress: protocol.setup.SetupProgress;
15+
private readonly onSetupProgressChangeEmitter = this.disposable.addDisposable(
16+
new Emitter<void>()
1117
);
12-
/**
13-
* Emitted when the setup progress is updated.
14-
*/
15-
public readonly onSetupProgressUpdate =
16-
this.onSetupProgressUpdateEmitter.event;
17-
18+
public readonly onSetupProgressChange =
19+
this.onSetupProgressChangeEmitter.event;
20+
get status() {
21+
return this.setupProgress.state;
22+
}
23+
get currentStepIndex() {
24+
return this.setupProgress.currentStepIndex;
25+
}
1826
constructor(
1927
sessionDisposable: Disposable,
2028
private pitcherClient: IPitcherClient
2129
) {
2230
sessionDisposable.onWillDispose(() => {
2331
this.disposable.dispose();
2432
});
33+
34+
// We have a race condition where we might not have the steps yet and need
35+
// an event to tell us when they have started. But we might also have all the steps,
36+
// where no new event will arrive. So we use a barrier to manage this
37+
const initialStepsBarrier = new Barrier<Step[]>();
38+
39+
this.setupProgress = this.pitcherClient.clients.setup.getProgress();
40+
this.steps = initialStepsBarrier
41+
.wait()
42+
.then((result) => (result.status === "resolved" ? result.value : []));
43+
44+
let hasInitializedSteps = Boolean(this.setupProgress.steps.length);
45+
46+
if (hasInitializedSteps) {
47+
initialStepsBarrier.open(
48+
this.setupProgress.steps.map(
49+
(step, index) => new Step(index, step, pitcherClient)
50+
)
51+
);
52+
}
53+
2554
this.disposable.addDisposable(
2655
pitcherClient.clients.setup.onSetupProgressUpdate((progress) => {
27-
this.onSetupProgressUpdateEmitter.fire(progress);
56+
if (!hasInitializedSteps) {
57+
hasInitializedSteps = true;
58+
initialStepsBarrier.open(
59+
progress.steps.map(
60+
(step, index) => new Step(index, step, pitcherClient)
61+
)
62+
);
63+
}
64+
65+
this.setupProgress = progress;
66+
this.onSetupProgressChangeEmitter.fire();
2867
})
2968
);
3069
}
3170

32-
/**
33-
* Run the setup tasks, this will prepare the docker image, and run the user defined
34-
* setup steps. This will automatically run when a sandbox is started.
35-
*/
36-
async run(): Promise<SetupProgress> {
37-
return this.pitcherClient.clients.setup.init();
71+
getSteps() {
72+
return this.steps;
3873
}
3974

40-
/**
41-
* Returns the current progress of the setup tasks.
42-
*/
43-
async getProgress(): Promise<SetupProgress> {
44-
await this.pitcherClient.clients.setup.readyPromise;
45-
return this.pitcherClient.clients.setup.getProgress();
75+
async run(): Promise<void> {
76+
await this.pitcherClient.clients.setup.init();
4677
}
4778

48-
async waitForFinish(): Promise<SetupProgress> {
49-
const progress = await this.getProgress();
50-
if (progress.state === "FINISHED") {
51-
return Promise.resolve(progress);
79+
async waitForFinish(): Promise<void> {
80+
if (this.setupProgress.state === "STOPPED") {
81+
throw new Error("Setup Failed");
5282
}
5383

54-
return listenOnce(this.onSetupProgressUpdate, (progress) => {
55-
return progress.state === "FINISHED";
84+
if (this.setupProgress.state === "FINISHED") {
85+
return;
86+
}
87+
88+
return new Promise<void>((resolve, reject) => {
89+
const disposer = this.onSetupProgressChange(() => {
90+
if (this.setupProgress.state === "FINISHED") {
91+
disposer.dispose();
92+
resolve();
93+
} else if (this.setupProgress.state === "STOPPED") {
94+
disposer.dispose();
95+
reject(new Error("Setup Failed"));
96+
}
97+
});
5698
});
5799
}
58100
}
59101

60-
export type SetupProgress = {
61-
state: "IDLE" | "IN_PROGRESS" | "FINISHED" | "STOPPED";
62-
steps: Step[];
63-
currentStepIndex: number;
64-
};
102+
export class Step {
103+
private disposable = new Disposable();
104+
// TODO: differentiate between stdout and stderr, also send back bytes instead of
105+
// strings
106+
private onOutputEmitter = this.disposable.addDisposable(
107+
new Emitter<string>()
108+
);
109+
public readonly onOutput = this.onOutputEmitter.event;
110+
private onStatusChangeEmitter = this.disposable.addDisposable(
111+
new Emitter<string>()
112+
);
113+
public readonly onStatusChange = this.onStatusChangeEmitter.event;
114+
private output: string[] = [];
115+
116+
get name(): string {
117+
return this.step.name;
118+
}
119+
120+
get command() {
121+
return this.step.command;
122+
}
65123

66-
export type SetupShellStatus = "SUCCEEDED" | "FAILED" | "SKIPPED";
124+
get status() {
125+
return this.step.finishStatus || "IDLE";
126+
}
127+
128+
constructor(
129+
stepIndex: number,
130+
private step: protocol.setup.Step,
131+
private pitcherClient: IPitcherClient
132+
) {
133+
this.disposable.addDisposable(
134+
this.pitcherClient.clients.setup.onSetupProgressUpdate((progress) => {
135+
const oldStep = this.step;
136+
const newStep = progress.steps[stepIndex];
67137

68-
export type Step = {
69-
name: string;
70-
command: string;
71-
shellId: Id | null;
72-
finishStatus: SetupShellStatus | null;
73-
};
138+
this.step = newStep;
139+
140+
if (newStep.finishStatus !== oldStep.finishStatus) {
141+
this.onStatusChangeEmitter.fire(newStep.finishStatus || "IDLE");
142+
}
143+
})
144+
);
145+
this.disposable.addDisposable(
146+
this.pitcherClient.clients.shell.onShellOut(({ shellId, out }) => {
147+
if (shellId === this.step.shellId) {
148+
this.onOutputEmitter.fire(out);
149+
150+
this.output.push(out);
151+
if (this.output.length > 1000) {
152+
this.output.shift();
153+
}
154+
}
155+
})
156+
);
157+
}
158+
159+
async open(dimensions = DEFAULT_SHELL_SIZE): Promise<string> {
160+
const open = async (shellId: protocol.shell.ShellId) => {
161+
const shell = await this.pitcherClient.clients.shell.open(
162+
shellId,
163+
dimensions
164+
);
165+
166+
this.output = shell.buffer;
167+
168+
return this.output.join("\n");
169+
};
170+
171+
if (this.step.shellId) {
172+
return open(this.step.shellId);
173+
}
174+
175+
return new Promise<string>((resolve) => {
176+
const disposable = this.onStatusChange(() => {
177+
if (this.step.shellId) {
178+
disposable.dispose();
179+
resolve(open(this.step.shellId));
180+
}
181+
});
182+
});
183+
}
184+
185+
async waitForFinish() {
186+
if (this.step.finishStatus === "FAILED") {
187+
throw new Error("Step Failed");
188+
}
189+
190+
if (
191+
this.step.finishStatus === "SUCCEEDED" ||
192+
this.step.finishStatus === "SKIPPED"
193+
) {
194+
return;
195+
}
196+
197+
return new Promise<void>((resolve, reject) => {
198+
const disposable = this.onStatusChange((status) => {
199+
if (status === "SUCCEEDED" || status === "SKIPPED") {
200+
disposable.dispose();
201+
resolve();
202+
} else if (status === "FAILED") {
203+
disposable.dispose();
204+
reject(new Error("Step Failed"));
205+
}
206+
});
207+
});
208+
}
209+
}

0 commit comments

Comments
 (0)