diff --git a/website/src/components/playground/Playground.tsx b/website/src/components/playground/Playground.tsx index 2d59941..9691f15 100644 --- a/website/src/components/playground/Playground.tsx +++ b/website/src/components/playground/Playground.tsx @@ -110,6 +110,12 @@ export function Playground() { useEffect(() => teardown, [teardown]); + // The terminal stays hidden until the VM is ready, and a hidden element + // can't take focus: focus it once the render that reveals it is done. + useEffect(() => { + if (status.kind === 'ready') termRef.current?.xterm.focus(); + }, [status.kind]); + const release = releases?.find((r) => r.tag === tag) ?? (picked?.tag === tag ? picked : undefined); @@ -151,7 +157,6 @@ export function Playground() { }, }); await enterShell(vm); - termRef.current.xterm.focus(); setStatus({ kind: 'ready', vcpus, restored }); } catch (e) { if (!ac.signal.aborted) setStatus({ kind: 'error', message: errorMessage(e) }); diff --git a/website/src/components/playground/vm.ts b/website/src/components/playground/vm.ts index 4fa41b8..ebf97b0 100644 --- a/website/src/components/playground/vm.ts +++ b/website/src/components/playground/vm.ts @@ -41,6 +41,8 @@ int fcntl64(int fd, int cmd, long arg) { return fcntl(fd, cmd, arg); } `; const HEPH_BIN = '/opt/heph/bin/heph'; const COMPAT_LIB = '/opt/heph/lib/libhephcompat.so'; +// heph's bash completion, generated by the installed release. +const COMPLETION = '/etc/heph-completion.bash'; type SetupStep = | { run: string } @@ -69,7 +71,12 @@ const BASE_SETUP: SetupStep[] = [ }, { write: '/etc/profile.d/heph.sh', - content: ['export PS1="\\w \\$ "', 'cat /etc/heph-welcome', ''].join('\n'), + content: [ + 'export PS1="\\w \\$ "', + `[ -n "$BASH_VERSION" ] && [ -r ${COMPLETION} ] && . ${COMPLETION}`, + 'cat /etc/heph-welcome', + '', + ].join('\n'), }, ]; @@ -82,7 +89,9 @@ const PLUGIN_OPTIONS: Record = { 'heph-go-plugin': ' options:\n gotool: host\n', }; -async function run(vm: Vm, command: string, timeoutMs = 300_000): Promise { +/** Run `commands` in one exec, stopping at the first that fails. */ +async function run(vm: Vm, commands: string | string[], timeoutMs = 300_000): Promise { + const command = [commands].flat().join(' && '); const { output, exitCode } = await vm.exec(command, { timeoutMs }); if (exitCode !== 0) { throw new Error(`\`${command}\` exited ${exitCode}:\n${output.slice(-2000)}`); @@ -272,15 +281,19 @@ export async function bootPlayground( const script = files.map(([path, content]) => ( `mkdir -p '${path.replace(/\/[^/]+$/, '')}'\ncat > '${path}' <<'HEPH_PLAYGROUND_EOF'\n${content}${content.endsWith('\n') ? '' : '\n'}HEPH_PLAYGROUND_EOF\n` )).join(''); - // Mounts the image lacks: pseudo-terminals for `heph run --shell`, and, - // for the examples, a filesystem with extended attributes, which - // `codegen = "copy"` needs (the root filesystem has none). - const mounts = [ + await vm.writeFile('/tmp/heph-install.sh', script); + await run(vm, [ + // Mounts the image lacks: pseudo-terminals for `heph run --shell`, and, + // for the examples, a filesystem with extended attributes, which + // `codegen = "copy"` needs (the root filesystem has none). 'mountpoint -q /dev/pts || { mkdir -p /dev/pts && mount -t devpts devpts /dev/pts; }', 'mkdir -p /root/examples && { mountpoint -q /root/examples || mount -t tmpfs tmpfs /root/examples; }', - ].join(' && '); - await vm.writeFile('/tmp/heph-install.sh', script); - await run(vm, `${mounts} && sh /tmp/heph-install.sh && rm /tmp/heph-install.sh && heph version`); + 'sh /tmp/heph-install.sh', + 'rm /tmp/heph-install.sh', + // Releases without `heph tool completions` just go without completion. + `{ heph tool completions bash > ${COMPLETION} 2>/dev/null || rm -f ${COMPLETION}; }`, + 'heph version', + ]); } catch (e) { vm.dispose(); throw e;