819e88eff38d

signal the build's process group by id, since koh's own guard skips an exited command, and hold it with a test that leaves a child behind (3.0.81)

time
2026-09-27 10:24 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
deepseek/deepseek-flash
session
koh 3 phases 6-8
parent
818acd921e18
4 files VERSION+1 −1src/build/build.v+11 −5src/build/build_test.v+19 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.801+3.0.81

view file

src/build/build.v+11 −5
@@ -7,6 +7,8 @@77 88 module build99 10+#include <signal.h>11+1012 import os1113 import time1214 @@ -321,15 +323,19 @@321323 		time.sleep(200 * time.millisecond)322324 	}323325 	timed_out := child.is_alive()326+	// Whatever the command left running goes with it: a background child could327+	// still be writing the artifact after it was hashed, and a hung build must328+	// not hold a release. The group is signalled by id — the command leads its329+	// own group — because `signal_pgkill` returns early once the process has330+	// exited. Nothing is signalled before the command started, when the id is 0331+	// and the group would be koh's own.332+	if child.pid > 0 {333+		C.kill(-child.pid, C.SIGKILL)334+	}324335 	if timed_out {325336 		// Kill first: waiting on a hung build waits forever.326-		child.signal_pgkill()327337 		child.wait()328338 	}329-	// Whatever the command left running goes with it: a background child could330-	// still be writing the artifact after it was hashed. Its own process group331-	// again, so a stray child dies and nothing else does.332-	child.signal_pgkill()333339 	stdout := os.read_file(stdout_path) or { '' }334340 	stderr := os.read_file(stderr_path) or { '' }335341 	return Run{

view file

src/build/build_test.v+19 −0
@@ -304,3 +304,22 @@304304 		assert err.msg().contains('produced an empty')305305 	}306306 }307+308+// A command that leaves something running in the background must not outlive309+// the build: it could still be writing the artifact after it was hashed.310+fn test_build_one_takes_background_children_with_it() {311+	root := scratch('build_orphan')!312+	defer {313+		os.rmdir_all(root) or {}314+	}315+	write(root, 'koh.build', "command = mkdir -p out && printf early > out/demo && { sh -c 'sleep 2; printf late > out/demo' & }316+output = out/demo317+platforms = macos-aarch64318+")!319+	config := read_config(root)!320+	built := build_one(root, config, 'macos-aarch64', '1.2.3')!321+	assert built.size == 5322+	// Give the background child its two seconds: it must be gone by now.323+	time.sleep(3 * time.second)324+	assert os.read_file(os.join_path(root, 'out/demo'))! == 'early'325+}

view file

v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 	name: 'koh3'3-	version: '3.0.80'3+	version: '3.0.81'44 	description: 'One SQLite file per repo, one line of history.'55 	license: 'MIT'66 }

view file