diff --git a/lib/hypervisor/qemu/config.go b/lib/hypervisor/qemu/config.go index 4b395c40..7c65af29 100644 --- a/lib/hypervisor/qemu/config.go +++ b/lib/hypervisor/qemu/config.go @@ -100,9 +100,16 @@ func BuildArgs(cfg hypervisor.VMConfig) []string { args = append(args, "-device", deviceArg) } - // Serial console output to file + // Serial console output to file. Use a chardev with append=on so QEMU + // opens the file with O_APPEND. Without it, QEMU writes at its internal + // fd offset; if the file is externally truncated (e.g. log rotation via + // copytruncate) subsequent writes leave a sparse hole of NUL bytes from + // byte 0 to the stale offset, which downstream log readers will pick up. if cfg.SerialLogPath != "" { - args = append(args, "-serial", fmt.Sprintf("file:%s", cfg.SerialLogPath)) + args = append(args, + "-chardev", fmt.Sprintf("file,id=serial0,path=%s,append=on", cfg.SerialLogPath), + "-serial", "chardev:serial0", + ) } else { args = append(args, "-serial", "stdio") } diff --git a/lib/hypervisor/qemu/config_test.go b/lib/hypervisor/qemu/config_test.go index 1cb64c93..f4a3e452 100644 --- a/lib/hypervisor/qemu/config_test.go +++ b/lib/hypervisor/qemu/config_test.go @@ -144,8 +144,10 @@ func TestBuildArgs_SerialLog(t *testing.T) { args := BuildArgs(cfg) + assert.Contains(t, args, "-chardev") + assert.Contains(t, args, "file,id=serial0,path=/var/log/app.log,append=on") assert.Contains(t, args, "-serial") - assert.Contains(t, args, "file:/var/log/app.log") + assert.Contains(t, args, "chardev:serial0") } func TestBuildArgs_NoSerialLog(t *testing.T) {