From f373abdd14ac1a4e3c5007f854042a9c7746a534 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:19:49 -0600 Subject: [PATCH] fix: Container runtime actions (#2723) Actions where running in a race condition that they sometimes didn't wait for the container to be started and the issue was the exec that was then run after would have an issue. --- sdk/lib/util/SubContainer.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/sdk/lib/util/SubContainer.ts b/sdk/lib/util/SubContainer.ts index 1cf9484bc..0d5982886 100644 --- a/sdk/lib/util/SubContainer.ts +++ b/sdk/lib/util/SubContainer.ts @@ -3,9 +3,10 @@ import * as T from "../types" import * as cp from "child_process" import { promisify } from "util" import { Buffer } from "node:buffer" +import { once } from "./once" export const execFile = promisify(cp.execFile) const WORKDIR = (imageId: string) => `/media/startos/images/${imageId}/` - +const False = () => false type ExecResults = { exitCode: number | null exitSignal: NodeJS.Signals | null @@ -17,6 +18,8 @@ export type ExecOptions = { input?: string | Buffer } +const TIMES_TO_WAIT_FOR_PROC = 100 + /** * This is the type that is going to describe what an subcontainer could do. The main point of the * subcontainer is to have commands that run in a chrooted environment. This is useful for running @@ -44,6 +47,7 @@ export interface ExecSpawnable { export class SubContainer implements ExecSpawnable { private leader: cp.ChildProcess private leaderExited: boolean = false + private waitProc: () => Promise private constructor( readonly effects: T.Effects, readonly imageId: T.ImageId, @@ -58,6 +62,26 @@ export class SubContainer implements ExecSpawnable { this.leader.on("exit", () => { this.leaderExited = true }) + this.waitProc = once( + () => + new Promise(async (resolve, reject) => { + let count = 0 + while ( + !(await fs.stat(`${this.rootfs}/proc/1`).then((x) => !!x, False)) + ) { + if (count++ > TIMES_TO_WAIT_FOR_PROC) { + console.debug("Failed to start subcontainer", { + guid: this.guid, + imageId: this.imageId, + rootfs: this.rootfs, + }) + reject(new Error(`Failed to start subcontainer ${this.imageId}`)) + } + await wait(1) + } + resolve() + }), + ) } static async of( effects: T.Effects, @@ -184,6 +208,7 @@ export class SubContainer implements ExecSpawnable { stdout: string | Buffer stderr: string | Buffer }> { + await this.waitProc() const imageMeta: T.ImageMetadata = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", @@ -266,6 +291,7 @@ export class SubContainer implements ExecSpawnable { command: string[], options?: CommandOptions, ): Promise { + await this.waitProc() const imageMeta: any = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", @@ -307,6 +333,7 @@ export class SubContainer implements ExecSpawnable { command: string[], options?: CommandOptions, ): Promise { + await this.waitProc() const imageMeta: any = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", @@ -402,3 +429,6 @@ export type MountOptionsBackup = { type: "backup" subpath: string | null } +function wait(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)) +}