Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions scripts/lume/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,18 @@ wait_for_ssh() {
local ssh_exit

for attempt in $(seq 1 60); do
# Use 'if' to suppress set -e so a failed SSH attempt does not abort the loop.
# Lume can return zero while reporting that the guest has no IP address yet.
# A probe of `true` is only ready when it is both successful and silent.
if ssh_output="$(lume ssh "${LUME_VM_NAME}" --user "${GUEST_USER}" --password "${GUEST_PASSWORD}" --timeout 10 "true" 2>&1)"; then
ssh_exit=0
else
ssh_exit=$?
fi

if [[ "${ssh_exit}" -eq 0 ]] && [[ -z "${ssh_output}" ]]; then
return 0
fi
ssh_exit=$?

log "wait_for_ssh attempt ${attempt}/60: exit=${ssh_exit} output=[${ssh_output}]"
sleep 5
done
Expand Down
43 changes: 43 additions & 0 deletions test/lume-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
expect(reconcile).toContain('spawn_detached');
expect(reconcile).toContain('"${SCRIPT_DIR}/run-slot.sh" --slot "${slot}"');
expect(read("scripts/lume/lib.sh")).toContain("default_guest_runner_path");
expect(read("scripts/lume/lib.sh")).toContain('[[ -z "${ssh_output}" ]]');
expect(read("scripts/lume/lib.sh")).toContain('local runner_version="${RUNNER_VERSION}"');
expect(read("scripts/lume/lib.sh")).toContain("RUNNER_PATH=${runner_path}");
expect(read("scripts/lume/lib.sh")).toContain("RUNNER_VERSION=${runner_version}");
Expand Down Expand Up @@ -123,6 +124,14 @@
expect(helper).toContain("request_runner_token");
});

test("does not treat Lume SSH error output as readiness when it exits zero", () => {
const result = runWaitForSshProbe("Error: VM 'macos-runner-slot-01' has no IP address. Wait for it to boot completely.");

expect(result.status).toBe(1);
expect(result.stderr).toContain("wait_for_ssh attempt 1/60: exit=0");
expect(result.stderr).toContain("has no IP address");
});

test("provisions pinned Sparkle tools in the Lume base VM", () => {
const provisionBase = read("scripts/lume/provision-base-vm.sh");
const sparkleInstaller = read("scripts/guest/install-sparkle-tools.sh");
Expand Down Expand Up @@ -243,3 +252,37 @@
function writeExecutable(filePath: string, contents: string): void {
fs.writeFileSync(filePath, `${contents}\n`, { encoding: "utf8", mode: 0o755 });
}

function runWaitForSshProbe(output: string): { status: number | null; stderr: string } {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "lume-ssh-probe-"));
writeExecutable(
path.join(directory, "lume"),
["#!/bin/bash", `printf '%s\\n' ${JSON.stringify(output)}`, "exit 0"].join("\n"),
);
writeExecutable(path.join(directory, "seq"), ["#!/bin/bash", "printf '1\\n'"].join("\n"));
writeExecutable(path.join(directory, "sleep"), ["#!/bin/bash", "exit 0"].join("\n"));

const result = spawnSync(
"bash",
[
"-c",
[
'source "$1"',
'log() { printf "%s\\n" "$*" >&2; }',
'LUME_VM_NAME=macos-runner-slot-01',
'GUEST_USER=lume',
'GUEST_PASSWORD=test-password',
"wait_for_ssh",
].join("\n"),
"bash",
path.resolve("scripts/lume/lib.sh"),
],
{
encoding: "utf8",
env: { ...process.env, PATH: `${directory}:${process.env.PATH ?? ""}` },
},
);

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium test

This shell command depends on an uncontrolled
absolute path
.
Comment on lines +265 to +284

fs.rmSync(directory, { force: true, recursive: true });
return { status: result.status, stderr: result.stderr };
}
Loading