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:
+65
-8
@@ -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")?;
|
||||
|
||||
Reference in New Issue
Block a user