feat: terminate when init process exits (--kernel)

Add an init script to the initramfs that traps shell exit and triggers
poweroff via /proc/sysrq-trigger. This ensures QEMU terminates cleanly
when the user exits the shell, rather than hanging indefinitely.

Changes:
- Add /init script to initramfs that runs as PID 1
- Use -no-reboot QEMU flag to exit on guest poweroff
- Simplify kernel command line using environment variables
- Init script handles hostname, device creation, and poweroff on exit
This commit is contained in:
2026-06-17 14:03:04 +02:00
parent 7a37f99030
commit 503578d648
+65 -8
View File
@@ -69,20 +69,17 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
// For initramfs boot, use rdinit= instead of init=
// No root= needed as initramfs becomes the rootfs
// 'quiet' suppresses kernel log messages for a cleaner console
// Use setsid with redirected stdio to get proper job control
// /dev/ttyS0 is created in the initramfs for serial console
// Set hostname before spawning shell
// The init script (added to initramfs) handles hostname, shell, and poweroff
let kernel_append = if let Some(ref cmd) = config.command {
let cmd_str = cmd.join(" ");
format!(
"console=ttyS0 quiet rdinit=/bin/sh -- -c \"echo {} >/etc/hostname; hostname {}; setsid sh -c 'exec {} </dev/ttyS0 >/dev/ttyS0 2>&1 -c {}'\"",
hostname, hostname, shell, cmd_str
"console=ttyS0 quiet ECR_SHELL={} ECR_CMD=\"{}\" ECR_HOSTNAME={}",
shell, cmd_str, hostname
)
} else {
// Default to interactive shell with proper job control
format!(
"console=ttyS0 quiet rdinit=/bin/sh -- -c \"echo {} >/etc/hostname; hostname {}; setsid sh -c 'exec {} </dev/ttyS0 >/dev/ttyS0 2>&1'\"",
hostname, hostname, shell
"console=ttyS0 quiet ECR_SHELL={} ECR_HOSTNAME={}",
shell, hostname
)
};
@@ -95,6 +92,7 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
// Build QEMU arguments
// -display none suppresses VGA/BIOS output
// -serial mon:stdio connects serial console to terminal with QEMU monitor muxed
// -no-reboot makes QEMU exit when the guest requests poweroff/reboot
let args = vec![
"-kernel".to_string(),
config.kernel_path.to_string_lossy().to_string(),
@@ -108,6 +106,7 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
"none".to_string(),
"-serial".to_string(),
"mon:stdio".to_string(),
"-no-reboot".to_string(),
"-netdev".to_string(),
"user,id=net0".to_string(),
"-device".to_string(),
@@ -285,6 +284,64 @@ fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result<Vec<u8>> {
.context("Failed to finish device node entry")?;
}
// Add the /init script that will be run as PID 1
// This script handles hostname setup, shell execution, and poweroff on exit
// Uses /proc/sysrq-trigger for poweroff since poweroff command may not be available
let init_script = r#"#!/bin/sh
# ECR init script - runs as PID 1
# Mount essential filesystems
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
# Set hostname from kernel cmdline
if [ -n "$ECR_HOSTNAME" ]; then
echo "$ECR_HOSTNAME" > /etc/hostname
hostname "$ECR_HOSTNAME"
fi
# Create console device if missing
mknod -m 600 /dev/console c 5 1 2>/dev/null || true
mknod -m 666 /dev/ttyS0 c 4 64 2>/dev/null || true
# Function to poweroff - use sysrq-trigger which works without external binaries
do_poweroff() {
# Silence kernel printk to suppress shutdown messages
echo 0 > /proc/sys/kernel/printk
# 'o' means power off, see Documentation/admin-guide/sysrq.rst
echo o > /proc/sysrq-trigger
# Fallback: infinite loop to prevent kernel panic
while true; do sleep 1; done
}
# Trap exit to ensure poweroff runs
trap do_poweroff EXIT
# Run the shell or command
if [ -n "$ECR_CMD" ]; then
# Run command with the specified shell
setsid sh -c "exec $ECR_SHELL </dev/ttyS0 >/dev/ttyS0 2>&1 -c '$ECR_CMD'"
else
# Run interactive shell
setsid sh -c "exec $ECR_SHELL </dev/ttyS0 >/dev/ttyS0 2>&1"
fi
"#;
let init_data = init_script.as_bytes();
let init_builder = NewcBuilder::new("init")
.mode(0o100755) // executable
.uid(0)
.gid(0)
.nlink(1)
.mtime(0);
let mut init_writer = init_builder.write(&mut archive, init_data.len() as u32);
init_writer.write_all(init_data)
.context("Failed to write init script to cpio archive")?;
init_writer.finish()
.context("Failed to finish init script entry")?;
// Write the trailer (takes ownership and returns the writer)
archive = newc::trailer(archive)
.context("Failed to write cpio trailer")?;