Thank you for the tip!
Gtk.Application
or Gtk.ApplicationWindow
indeed seems to do some magic when spawned from the same program. When using only a Gtk.Window
i can now easily stop wat
spawned from wat
. (and is now also shown in ps aux
)
So the first problem is solved 
But spawning a bash script, bash -c
, make
, or entr
apparently does some other magic that i just cannot stop as simple as ctrl+c in bash terminal.
I don’t understand much, but just found these:
And i only got this to work: pstree -A -p $pid | grep -Eow '[0-9]+' | xargs kill

So here’s my stupid? newbie test that seems to work for my bash/make use-case
:
# makefile
.PHONY: build hot
build:
valac -X -w --pkg gtk4 grop.vala
hot:
ls *.vala | entr -rcs 'make build && ./grop'
// grop.vala
public class Thing : Gtk.Window
{
Pid pid;
int stdout;
int stderr;
Gtk.TextView text_view;
public Thing (string cmdline)
{
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
var print_btn = new Gtk.Button.with_label("print");
var spawn_tgl = new Gtk.ToggleButton.with_label("start");
text_view = new Gtk.TextView();
text_view.hexpand = true;
text_view.vexpand = true;
text_view.monospace = true;
default_width = 640;
default_height = 480;
print_btn.clicked.connect(()=>{
print("hello world\n");
});
spawn_tgl.toggled.connect(()=>{
bool run = spawn_tgl.get_active();
if (run) {
spawn_tgl.label = "stop";
print("START -------------\n");
spawn_cmdline(cmdline);
} else {
spawn_tgl.label = "start";
print("STOP --------------\n");
kill_cmdline();
}
});
box.append(print_btn);
box.append(spawn_tgl);
box.append(text_view);
set_child(box);
}
private bool process_line (IOChannel channel, IOCondition condition, string stream_name)
{
if (condition == IOCondition.HUP) {
return false;
}
try {
string line;
channel.read_line (out line, null, null);
//print ("%s: %s", stream_name, line);
text_view.buffer.text += @"$stream_name: $line";
} catch (IOChannelError e) {
print ("%s: IOChannelError: %s\n", stream_name, e.message);
return false;
} catch (ConvertError e) {
print ("%s: ConvertError: %s\n", stream_name, e.message);
return false;
}
return true;
}
private void kill_cmdline ()
{
string[] cmd = {"bash", "-c",
@"pstree -A -p $pid | grep -Eow '[0-9]+' | xargs kill"};
string[] env = Environ.get();
print(@"Kill pid tree: $pid\n");
try {
Process.spawn_sync(
".", cmd, env, SpawnFlags.SEARCH_PATH, null, null, null, null
);
} catch (SpawnError e) {
print(@"ERROR killing pid $pid: $(e.message)\n");
}
}
private void spawn_cmdline (string cmdline)
{
string[] cmd = {"bash", "-c", cmdline};
string[] env = Environ.get();
try {
Process.spawn_async_with_pipes(
".", cmd, env, SpawnFlags.SEARCH_PATH,
null,
out pid,
out stdout,
out stderr
);
print(@"Pid on start: $pid\n");
IOChannel output = new IOChannel.unix_new(stdout);
output.add_watch(IOCondition.IN | IOCondition.HUP, (channel, condition) => {
return process_line (channel, condition, "stdout");
});
IOChannel error = new IOChannel.unix_new(stderr);
error.add_watch(IOCondition.IN | IOCondition.HUP, (channel, condition) => {
return process_line (channel, condition, "stderr");
});
} catch (SpawnError e) {
print(@"ERROR: $(e.message)\n");
}
}
}
int main (string[] args) {
Gtk.init();
//var thing = new Thing("./grop");
var thing = new Thing("make hot");
thing.present();
while (Gtk.Window.get_toplevels ().get_n_items () > 0) {
MainContext.@default ().iteration (true);
}
return 0;
}
usage:
- build and run it:
make && ./grop
- click “print”
- should see “hello world” in terminal
- toggle “start”
- should see messages in first window textview
- should see a new grop window
- click “print” in second grop window
- should see messages in first window textview
- click “stop” in first grop window
- should stop second grop window, and make etc.