mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
feat(adapter-utils): instrument sandbox sync with per-stage duration timers
Add tar -> upload -> extract duration timers around the git, workspace overlay, and asset upload seams in prepareSandboxManagedRuntime, emitted per artifact + per stage under the existing git_sync/config_sync phase labels. Each line carries a placeholder transport tag (native|fallback, default fallback) for later phases to populate. Observability only: no transport behavior change. Establishes the measurement baseline for before/after sync performance comparison. Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
5 files changed
+298
-53
No files matched your search
@@ -65,7 +65,7 @@ export {
|
||||
redactCommandText,
|
||||
} from "./command-redaction.js";
|
||||
export { buildSandboxNpmInstallCommand } from "./sandbox-install-command.js";
|
||||
export { createRuntimeProgressReporter } from "./runtime-progress.js";
|
||||
export { createRuntimeProgressReporter, createSyncStageTimer } from "./runtime-progress.js";
|
||||
export type {
|
||||
RuntimeProgressSink,
|
||||
RuntimeProgressPhase,
|
||||
@@ -76,6 +76,9 @@ export type {
|
||||
RuntimeStatusPhase,
|
||||
RuntimeStatusSink,
|
||||
RuntimeStatusUpdate,
|
||||
SandboxSyncTransport,
|
||||
SyncStageTimer,
|
||||
SyncStageTimerOptions,
|
||||
} from "./runtime-progress.js";
|
||||
export { inferOpenAiCompatibleBiller } from "./billing.js";
|
||||
// Keep the root adapter-utils entry browser-safe because the UI imports it.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRuntimeProgressReporter } from "./runtime-progress.js";
|
||||
import { createRuntimeProgressReporter, createSyncStageTimer } from "./runtime-progress.js";
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
@@ -279,3 +279,110 @@ describe("createRuntimeProgressReporter", () => {
|
||||
expect(failLines.at(-1)).toContain("failed at 20%");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSyncStageTimer", () => {
|
||||
it("emits a per-stage + total duration line with the phase, artifact and transport tag", async () => {
|
||||
const clock = makeClock();
|
||||
const lines: string[] = [];
|
||||
const timer = createSyncStageTimer({
|
||||
sink: (line) => {
|
||||
lines.push(line);
|
||||
},
|
||||
phase: "git_sync",
|
||||
artifact: "git history",
|
||||
now: clock.now,
|
||||
});
|
||||
|
||||
await timer.time("tar", async () => {
|
||||
clock.advance(12);
|
||||
});
|
||||
await timer.time("upload", async () => {
|
||||
clock.advance(340);
|
||||
});
|
||||
await timer.time("extract", async () => {
|
||||
clock.advance(88);
|
||||
});
|
||||
await timer.finish();
|
||||
|
||||
expect(lines).toEqual([
|
||||
"[paperclip] sync git_sync git history: tar 12ms, upload 340ms, extract 88ms (total 440ms) [transport=fallback]\n",
|
||||
]);
|
||||
});
|
||||
|
||||
it("defaults the transport tag to fallback and honors an explicit native tag", async () => {
|
||||
const clock = makeClock();
|
||||
const native: string[] = [];
|
||||
const timer = createSyncStageTimer({
|
||||
sink: (line) => {
|
||||
native.push(line);
|
||||
},
|
||||
phase: "config_sync",
|
||||
artifact: "workspace",
|
||||
transport: "native",
|
||||
now: clock.now,
|
||||
});
|
||||
await timer.time("upload", async () => {
|
||||
clock.advance(5);
|
||||
});
|
||||
await timer.finish();
|
||||
expect(native).toEqual([
|
||||
"[paperclip] sync config_sync workspace: upload 5ms (total 5ms) [transport=native]\n",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns the timed function's result and records duration even when it throws", async () => {
|
||||
const clock = makeClock();
|
||||
const lines: string[] = [];
|
||||
const timer = createSyncStageTimer({
|
||||
sink: (line) => {
|
||||
lines.push(line);
|
||||
},
|
||||
phase: "config_sync",
|
||||
artifact: "skills",
|
||||
now: clock.now,
|
||||
});
|
||||
|
||||
const value = await timer.time("tar", async () => {
|
||||
clock.advance(7);
|
||||
return 42;
|
||||
});
|
||||
expect(value).toBe(42);
|
||||
|
||||
await expect(
|
||||
timer.time("upload", async () => {
|
||||
clock.advance(3);
|
||||
throw new Error("upload boom");
|
||||
}),
|
||||
).rejects.toThrow("upload boom");
|
||||
|
||||
await timer.finish();
|
||||
expect(lines).toEqual([
|
||||
"[paperclip] sync config_sync skills: tar 7ms, upload 3ms (total 10ms) [transport=fallback]\n",
|
||||
]);
|
||||
});
|
||||
|
||||
it("is idempotent on finish and a no-op with no sink or no timed stage", async () => {
|
||||
const lines: string[] = [];
|
||||
const timer = createSyncStageTimer({
|
||||
sink: (line) => {
|
||||
lines.push(line);
|
||||
},
|
||||
phase: "git_sync",
|
||||
artifact: "git history",
|
||||
now: makeClock().now,
|
||||
});
|
||||
// No stage timed → nothing to report.
|
||||
await timer.finish();
|
||||
await timer.finish();
|
||||
expect(lines).toEqual([]);
|
||||
|
||||
// Disabled sink → never throws, emits nothing.
|
||||
const disabled = createSyncStageTimer({
|
||||
sink: undefined,
|
||||
phase: "git_sync",
|
||||
artifact: "git history",
|
||||
});
|
||||
await disabled.time("tar", async () => {});
|
||||
await expect(disabled.finish()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,75 @@ export interface RuntimeStatusUpdate {
|
||||
|
||||
export type RuntimeStatusSink = (update: RuntimeStatusUpdate) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Which transport carried a sync artifact's bytes. Phase 0 (PAP-2952) always
|
||||
* records the base64-shell `"fallback"`; later phases populate `"native"` once
|
||||
* the sandbox seam gains a direct upload verb. Placeholder tag on the timer line
|
||||
* so Phase 6 can compare fallback-vs-native durations from the same log shape.
|
||||
*/
|
||||
export type SandboxSyncTransport = "native" | "fallback";
|
||||
|
||||
export interface SyncStageTimerOptions {
|
||||
/** Where the aggregated duration line is written (the run log). Undefined = disabled. */
|
||||
sink: RuntimeProgressSink | undefined;
|
||||
/** Reused status-phase label, e.g. "git_sync" / "config_sync". */
|
||||
phase: RuntimeStatusPhase;
|
||||
/** Per-artifact label, e.g. "git history", "workspace", or an asset key. */
|
||||
artifact: string;
|
||||
/** Transport tag placeholder; defaults to "fallback" (see {@link SandboxSyncTransport}). */
|
||||
transport?: SandboxSyncTransport;
|
||||
/** Injectable clock for deterministic tests. Default `Date.now`. */
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
export interface SyncStageTimer {
|
||||
/**
|
||||
* Run `fn`, recording its wall-clock duration under `stage`. Returns `fn`'s
|
||||
* result and re-throws its error; the duration is recorded either way so a
|
||||
* failing stage still contributes to the breakdown.
|
||||
*/
|
||||
time<T>(stage: string, fn: () => Promise<T>): Promise<T>;
|
||||
/**
|
||||
* Emit the aggregated per-stage + total duration line for this artifact.
|
||||
* Idempotent; a no-op when no sink was provided or no stage was timed.
|
||||
*/
|
||||
finish(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration instrumentation for a single sync artifact's tar → upload → extract
|
||||
* seams. Each stage is wrapped with `time()`; `finish()` emits one legible line
|
||||
* carrying the per-stage durations, the total, and the transport tag — the
|
||||
* measurement baseline Phase 6 compares before/after.
|
||||
*/
|
||||
export function createSyncStageTimer(options: SyncStageTimerOptions): SyncStageTimer {
|
||||
const now = options.now ?? Date.now;
|
||||
const transport: SandboxSyncTransport = options.transport ?? "fallback";
|
||||
const stages: { stage: string; durationMs: number }[] = [];
|
||||
let finished = false;
|
||||
|
||||
return {
|
||||
async time(stage, fn) {
|
||||
const startedAt = now();
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
stages.push({ stage, durationMs: Math.max(0, Math.round(now() - startedAt)) });
|
||||
}
|
||||
},
|
||||
async finish() {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
if (!options.sink || stages.length === 0) return;
|
||||
const totalMs = stages.reduce((sum, entry) => sum + entry.durationMs, 0);
|
||||
const breakdown = stages.map((entry) => `${entry.stage} ${entry.durationMs}ms`).join(", ");
|
||||
await options.sink(
|
||||
`[paperclip] sync ${options.phase} ${options.artifact}: ${breakdown} (total ${totalMs}ms) [transport=${transport}]\n`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface RuntimeProgressReporterOptions {
|
||||
sink: RuntimeProgressSink;
|
||||
phase: RuntimeProgressPhase;
|
||||
|
||||
@@ -225,6 +225,7 @@ describe("sandbox managed runtime", () => {
|
||||
},
|
||||
};
|
||||
const runtimeStatuses: Array<{ phase: string; message: string }> = [];
|
||||
const progressLines: string[] = [];
|
||||
|
||||
const prepared = await prepareSandboxManagedRuntime({
|
||||
spec: {
|
||||
@@ -238,6 +239,9 @@ describe("sandbox managed runtime", () => {
|
||||
adapterKey: "test-adapter",
|
||||
client,
|
||||
workspaceLocalDir: localWorkspaceDir,
|
||||
onProgress: (line) => {
|
||||
progressLines.push(line);
|
||||
},
|
||||
onRuntimeProgress: async (status) => {
|
||||
runtimeStatuses.push({ phase: status.phase, message: status.message });
|
||||
},
|
||||
@@ -300,6 +304,15 @@ describe("sandbox managed runtime", () => {
|
||||
status.phase === "export" &&
|
||||
/^Exporting git history from sandbox: 100% \(\d+\.\d\/\d+\.\d MB\)$/.test(status.message)
|
||||
))).toBe(true);
|
||||
|
||||
// Sync-duration timers cover the git and workspace-overlay phases too
|
||||
// (PAP-2952 Phase 0): one tar → upload → extract breakdown per artifact.
|
||||
expect(progressLines.some((line) =>
|
||||
/^\[paperclip\] sync git_sync git history: tar \d+ms, upload \d+ms, extract \d+ms \(total \d+ms\) \[transport=fallback\]\n$/.test(line),
|
||||
)).toBe(true);
|
||||
expect(progressLines.some((line) =>
|
||||
/^\[paperclip\] sync config_sync workspace: tar \d+ms, upload \d+ms, extract \d+ms \(total \d+ms\) \[transport=fallback\]\n$/.test(line),
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it("repairs stale host index deletions when the sandbox restores a clean git worktree", async () => {
|
||||
@@ -757,6 +770,18 @@ describe("sandbox managed runtime", () => {
|
||||
const uploadAssetLines = lines.filter((line) => line.includes("Syncing skills to sandbox"));
|
||||
expect(uploadWorkspaceLines.length).toBeGreaterThan(0);
|
||||
expect(uploadAssetLines.length).toBeGreaterThan(0);
|
||||
|
||||
// Per-artifact sync-duration timer lines (PAP-2952 Phase 0): tar → upload →
|
||||
// extract breakdown + total, tagged with the placeholder transport, one per
|
||||
// synced artifact (workspace + the skills asset here).
|
||||
const workspaceTimer = lines.find((line) =>
|
||||
/^\[paperclip\] sync config_sync workspace: tar \d+ms, upload \d+ms, extract \d+ms \(total \d+ms\) \[transport=fallback\]\n$/.test(line),
|
||||
);
|
||||
const assetTimer = lines.find((line) =>
|
||||
/^\[paperclip\] sync config_sync skills: tar \d+ms, upload \d+ms, extract \d+ms \(total \d+ms\) \[transport=fallback\]\n$/.test(line),
|
||||
);
|
||||
expect(workspaceTimer).toBeDefined();
|
||||
expect(assetTimer).toBeDefined();
|
||||
// 100 reported increments must be throttled to at most ~one line per 10% step.
|
||||
expect(uploadWorkspaceLines.length).toBeLessThanOrEqual(11);
|
||||
// Reaches 100% and shows the MB breakdown.
|
||||
|
||||
@@ -18,11 +18,13 @@ import {
|
||||
import { captureDirectorySnapshot, mergeDirectoryWithBaseline } from "./workspace-restore-merge.js";
|
||||
import {
|
||||
createRuntimeProgressReporter,
|
||||
createSyncStageTimer,
|
||||
type RuntimeProgressDirection,
|
||||
type RuntimeProgressPhase,
|
||||
type RuntimeProgressSink,
|
||||
type RuntimeStatusPhase,
|
||||
type RuntimeStatusSink,
|
||||
type SandboxSyncTransport,
|
||||
} from "./runtime-progress.js";
|
||||
import { isRelativePathOrDescendant, shouldExcludePath } from "./exclude-patterns.js";
|
||||
|
||||
@@ -494,7 +496,13 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
// child task wires it into writeFile/readFile.
|
||||
onProgress?: RuntimeProgressSink;
|
||||
onRuntimeProgress?: RuntimeStatusSink;
|
||||
// Placeholder transport tag for the sync-duration instrumentation. Phase 0
|
||||
// (PAP-2952) always records the base64-shell "fallback"; later phases set
|
||||
// "native" once the seam gains a direct upload verb. Only labels the timer
|
||||
// lines — it does not change transport behavior.
|
||||
syncTransport?: SandboxSyncTransport;
|
||||
}): Promise<PreparedSandboxManagedRuntime> {
|
||||
const syncTransport: SandboxSyncTransport = input.syncTransport ?? "fallback";
|
||||
const workspaceRemoteDir = input.workspaceRemoteDir ?? input.spec.remoteCwd;
|
||||
const runtimeRootDir = path.posix.join(workspaceRemoteDir, ".paperclip-runtime", input.adapterKey);
|
||||
const gitSnapshot = await readGitWorkspaceSnapshot(input.workspaceLocalDir);
|
||||
@@ -525,17 +533,25 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
]);
|
||||
if (gitSnapshot) {
|
||||
await emitRuntimeStatus(input.onRuntimeProgress, "git_sync", "Syncing git history to sandbox");
|
||||
const gitTimer = createSyncStageTimer({
|
||||
sink: input.onProgress,
|
||||
phase: "git_sync",
|
||||
artifact: "git history",
|
||||
transport: syncTransport,
|
||||
});
|
||||
await withShallowGitWorkspaceClone({
|
||||
localDir: input.workspaceLocalDir,
|
||||
snapshot: gitSnapshot,
|
||||
}, async (cloneDir) => {
|
||||
const gitTarPath = path.join(tempDir, "git-workspace.tar");
|
||||
await createTarballFromDirectory({
|
||||
localDir: cloneDir,
|
||||
archivePath: gitTarPath,
|
||||
exclude: [".paperclip-runtime"],
|
||||
const gitTarBytes = await gitTimer.time("tar", async () => {
|
||||
await createTarballFromDirectory({
|
||||
localDir: cloneDir,
|
||||
archivePath: gitTarPath,
|
||||
exclude: [".paperclip-runtime"],
|
||||
});
|
||||
return fs.readFile(gitTarPath);
|
||||
});
|
||||
const gitTarBytes = await fs.readFile(gitTarPath);
|
||||
const remoteGitTar = path.posix.join(runtimeRootDir, "git-workspace-upload.tar");
|
||||
await input.client.makeDir(runtimeRootDir);
|
||||
const gitUpload = makeTransferProgress(
|
||||
@@ -545,9 +561,11 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
"git history",
|
||||
{ sink: input.onRuntimeProgress, phase: "git_sync" },
|
||||
);
|
||||
await input.client.writeFile(remoteGitTar, toArrayBuffer(gitTarBytes), gitUpload.options);
|
||||
await gitUpload.finish(gitTarBytes.byteLength, gitTarBytes.byteLength);
|
||||
await input.client.run(
|
||||
await gitTimer.time("upload", async () => {
|
||||
await input.client.writeFile(remoteGitTar, toArrayBuffer(gitTarBytes), gitUpload.options);
|
||||
await gitUpload.finish(gitTarBytes.byteLength, gitTarBytes.byteLength);
|
||||
});
|
||||
await gitTimer.time("extract", () => input.client.run(
|
||||
`sh -c ${shellQuote(
|
||||
`mkdir -p ${shellQuote(workspaceRemoteDir)} && ` +
|
||||
`find ${shellQuote(workspaceRemoteDir)} -mindepth 1 -maxdepth 1 ${preserveFindArgs([".paperclip-runtime"])} -exec rm -rf -- {} + && ` +
|
||||
@@ -555,27 +573,36 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
`rm -f ${shellQuote(remoteGitTar)}`,
|
||||
)}`,
|
||||
{ timeoutMs: input.spec.timeoutMs },
|
||||
);
|
||||
));
|
||||
});
|
||||
await gitTimer.finish();
|
||||
}
|
||||
|
||||
const workspaceTarPath = path.join(tempDir, "workspace.tar");
|
||||
const workspaceArchiveDir = gitSnapshot ? path.join(tempDir, "workspace-overlay") : input.workspaceLocalDir;
|
||||
await emitRuntimeStatus(input.onRuntimeProgress, "config_sync", "Syncing workspace to sandbox");
|
||||
if (gitSnapshot) {
|
||||
await copySelectedWorkspaceEntries({
|
||||
sourceDir: input.workspaceLocalDir,
|
||||
targetDir: workspaceArchiveDir,
|
||||
relativePaths: gitSnapshot.overlayPaths,
|
||||
exclude: workspaceArchiveExclude,
|
||||
const workspaceTimer = createSyncStageTimer({
|
||||
sink: input.onProgress,
|
||||
phase: "config_sync",
|
||||
artifact: "workspace",
|
||||
transport: syncTransport,
|
||||
});
|
||||
const workspaceTarBytes = await workspaceTimer.time("tar", async () => {
|
||||
if (gitSnapshot) {
|
||||
await copySelectedWorkspaceEntries({
|
||||
sourceDir: input.workspaceLocalDir,
|
||||
targetDir: workspaceArchiveDir,
|
||||
relativePaths: gitSnapshot.overlayPaths,
|
||||
exclude: workspaceArchiveExclude,
|
||||
});
|
||||
}
|
||||
await createTarballFromDirectory({
|
||||
localDir: workspaceArchiveDir,
|
||||
archivePath: workspaceTarPath,
|
||||
exclude: gitSnapshot ? undefined : workspaceArchiveExclude,
|
||||
});
|
||||
}
|
||||
await createTarballFromDirectory({
|
||||
localDir: workspaceArchiveDir,
|
||||
archivePath: workspaceTarPath,
|
||||
exclude: gitSnapshot ? undefined : workspaceArchiveExclude,
|
||||
return fs.readFile(workspaceTarPath);
|
||||
});
|
||||
const workspaceTarBytes = await fs.readFile(workspaceTarPath);
|
||||
const remoteWorkspaceTar = path.posix.join(runtimeRootDir, "workspace-upload.tar");
|
||||
await input.client.makeDir(runtimeRootDir);
|
||||
const workspaceUpload = makeTransferProgress(
|
||||
@@ -585,12 +612,14 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
"workspace",
|
||||
{ sink: input.onRuntimeProgress, phase: "config_sync" },
|
||||
);
|
||||
await input.client.writeFile(
|
||||
remoteWorkspaceTar,
|
||||
toArrayBuffer(workspaceTarBytes),
|
||||
workspaceUpload.options,
|
||||
);
|
||||
await workspaceUpload.finish(workspaceTarBytes.byteLength, workspaceTarBytes.byteLength);
|
||||
await workspaceTimer.time("upload", async () => {
|
||||
await input.client.writeFile(
|
||||
remoteWorkspaceTar,
|
||||
toArrayBuffer(workspaceTarBytes),
|
||||
workspaceUpload.options,
|
||||
);
|
||||
await workspaceUpload.finish(workspaceTarBytes.byteLength, workspaceTarBytes.byteLength);
|
||||
});
|
||||
const extractWorkspaceTarCommand = gitSnapshot
|
||||
? `mkdir -p ${shellQuote(workspaceRemoteDir)} && ` +
|
||||
`tar -xf ${shellQuote(remoteWorkspaceTar)} -C ${shellQuote(workspaceRemoteDir)} && ` +
|
||||
@@ -599,10 +628,11 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
`find ${shellQuote(workspaceRemoteDir)} -mindepth 1 -maxdepth 1 ${preserveFindArgs([...preservedNames])} -exec rm -rf -- {} + && ` +
|
||||
`tar -xf ${shellQuote(remoteWorkspaceTar)} -C ${shellQuote(workspaceRemoteDir)} && ` +
|
||||
`rm -f ${shellQuote(remoteWorkspaceTar)}`;
|
||||
await input.client.run(
|
||||
await workspaceTimer.time("extract", () => input.client.run(
|
||||
`sh -c ${shellQuote(extractWorkspaceTarCommand)}`,
|
||||
{ timeoutMs: input.spec.timeoutMs },
|
||||
);
|
||||
));
|
||||
await workspaceTimer.finish();
|
||||
if (gitSnapshot) {
|
||||
await removeDeletedPathsInSandbox({
|
||||
client: input.client,
|
||||
@@ -614,14 +644,22 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
|
||||
for (const asset of input.assets ?? []) {
|
||||
await emitRuntimeStatus(input.onRuntimeProgress, "config_sync", "Syncing runtime assets to sandbox");
|
||||
const assetTimer = createSyncStageTimer({
|
||||
sink: input.onProgress,
|
||||
phase: "config_sync",
|
||||
artifact: asset.key,
|
||||
transport: syncTransport,
|
||||
});
|
||||
const assetTarPath = path.join(tempDir, `${asset.key}.tar`);
|
||||
await createTarballFromDirectory({
|
||||
localDir: asset.localDir,
|
||||
archivePath: assetTarPath,
|
||||
followSymlinks: asset.followSymlinks,
|
||||
exclude: asset.exclude,
|
||||
const assetTarBytes = await assetTimer.time("tar", async () => {
|
||||
await createTarballFromDirectory({
|
||||
localDir: asset.localDir,
|
||||
archivePath: assetTarPath,
|
||||
followSymlinks: asset.followSymlinks,
|
||||
exclude: asset.exclude,
|
||||
});
|
||||
return fs.readFile(assetTarPath);
|
||||
});
|
||||
const assetTarBytes = await fs.readFile(assetTarPath);
|
||||
const remoteAssetDir = path.posix.join(runtimeRootDir, asset.key);
|
||||
const remoteAssetTar = path.posix.join(runtimeRootDir, `${asset.key}-upload.tar`);
|
||||
const assetUpload = makeTransferProgress(
|
||||
@@ -631,30 +669,33 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
asset.key,
|
||||
{ sink: input.onRuntimeProgress, phase: "config_sync" },
|
||||
);
|
||||
await input.client.writeFile(remoteAssetTar, toArrayBuffer(assetTarBytes), assetUpload.options);
|
||||
await assetUpload.finish(assetTarBytes.byteLength, assetTarBytes.byteLength);
|
||||
for (const stageFile of asset.provision?.stageFiles ?? []) {
|
||||
const stageBytes = typeof stageFile.contents === "string"
|
||||
? Buffer.from(stageFile.contents)
|
||||
: stageFile.contents;
|
||||
const safeName = stageFile.name;
|
||||
if (/[\\/]|\.\.(\.|$)/.test(safeName) || safeName === "..") {
|
||||
throw new Error(`provision stageFile.name must be a simple basename, got: ${safeName}`);
|
||||
await assetTimer.time("upload", async () => {
|
||||
await input.client.writeFile(remoteAssetTar, toArrayBuffer(assetTarBytes), assetUpload.options);
|
||||
await assetUpload.finish(assetTarBytes.byteLength, assetTarBytes.byteLength);
|
||||
for (const stageFile of asset.provision?.stageFiles ?? []) {
|
||||
const stageBytes = typeof stageFile.contents === "string"
|
||||
? Buffer.from(stageFile.contents)
|
||||
: stageFile.contents;
|
||||
const safeName = stageFile.name;
|
||||
if (/[\\/]|\.\.(\.|$)/.test(safeName) || safeName === "..") {
|
||||
throw new Error(`provision stageFile.name must be a simple basename, got: ${safeName}`);
|
||||
}
|
||||
await input.client.writeFile(
|
||||
path.posix.join(runtimeRootDir, safeName),
|
||||
toArrayBuffer(stageBytes),
|
||||
);
|
||||
}
|
||||
await input.client.writeFile(
|
||||
path.posix.join(runtimeRootDir, safeName),
|
||||
toArrayBuffer(stageBytes),
|
||||
);
|
||||
}
|
||||
});
|
||||
const extractCommand = asset.provision?.extractCommand?.({
|
||||
assetTarPath: remoteAssetTar,
|
||||
assetDir: remoteAssetDir,
|
||||
runtimeRootDir,
|
||||
}) ?? buildDefaultExtractRuntimeAssetCommand({ remoteAssetDir, remoteAssetTar });
|
||||
await input.client.run(
|
||||
await assetTimer.time("extract", () => input.client.run(
|
||||
`sh -c ${shellQuote(extractCommand)}`,
|
||||
{ timeoutMs: input.spec.timeoutMs },
|
||||
);
|
||||
));
|
||||
await assetTimer.finish();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user