From 0d5bcff353d8947619ce325f8c199cce55a34971 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 5 Jan 2025 10:50:50 +0530 Subject: [PATCH] Tool to implement robust cleanups even on process crash --- kitty_tests/atexit.py | 72 ++++++++++++++++++++++++++++++++++++++++ tools/cmd/atexit/main.go | 65 ++++++++++++++++++++++++++++++++++++ tools/cmd/tool/main.go | 3 ++ 3 files changed, 140 insertions(+) create mode 100644 kitty_tests/atexit.py create mode 100644 tools/cmd/atexit/main.go diff --git a/kitty_tests/atexit.py b/kitty_tests/atexit.py new file mode 100644 index 00000000000..a09433427af --- /dev/null +++ b/kitty_tests/atexit.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2025, Kovid Goyal + + +import os +import select +import shutil +import signal +import subprocess +import tempfile + +from kitty.constants import kitten_exe, kitty_exe + +from . import BaseTest + + +class Atexit(BaseTest): + + def setUp(self): + self.tdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tdir) + + def test_atexit(self): + + def r(action='close'): + p = subprocess.Popen([kitty_exe(), '+runpy', f'''\ +import subprocess +p = subprocess.Popen(['{kitten_exe()}', '__atexit__']) +print(p.pid, flush=True) +raise SystemExit(p.wait()) +'''], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + readers = [p.stdout.fileno()] + def read(): + r, _, _ = select.select(readers, [], [], 10) + if not r: + raise TimeoutError('Timed out waiting for read from child') + return p.stdout.readline().rstrip().decode() + atexit_pid = int(read()) + for i in range(2): + with open(os.path.join(self.tdir, str(i)), 'w') as f: + p.stdin.write(f'unlink {f.name}\n'.encode()) + p.stdin.flush() + select.select(readers, [], [], 10) + self.ae(read(), str(i+1)) + self.assertTrue(os.listdir(self.tdir)) + + # Ensure child is ignoring signals + os.kill(atexit_pid, signal.SIGINT) + os.kill(atexit_pid, signal.SIGTERM) + if action == 'close': + p.stdin.close() + elif action == 'terminate': + p.terminate() + else: + p.kill() + p.wait(10) + if action != 'close': + p.stdin.close() + select.select(readers, [], [], 10) + self.assertFalse(read()) + p.stdout.close() + self.assertFalse(os.listdir(self.tdir)) + try: + os.waitpid(atexit_pid, 0) + except ChildProcessError: + pass + + r('close') + r('terminate') + r('kill') diff --git a/tools/cmd/atexit/main.go b/tools/cmd/atexit/main.go new file mode 100644 index 00000000000..9735c5ab6aa --- /dev/null +++ b/tools/cmd/atexit/main.go @@ -0,0 +1,65 @@ +package atexit + +import ( + "bufio" + "fmt" + "os" + "os/signal" + "strings" + + "kitty/tools/cli" +) + +var _ = fmt.Print + +func main() (rc int, err error) { + signal.Ignore() + done_channel := make(chan bool) + lines := []string{} + + defer os.Stdout.Close() + + go func() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + fmt.Println(len(lines)) + } + done_channel <- true + }() + + keep_going := true + for keep_going { + select { + case <-done_channel: + keep_going = false + } + } + rc = 0 + for _, line := range lines { + if action, rest, found := strings.Cut(line, " "); found { + switch action { + case "unlink": + if err := os.Remove(rest); err != nil { + fmt.Fprintln(os.Stderr, "Failed to remove:", rest, "with error:", err) + rc = 1 + } + } + } + } + return +} + +func EntryPoint(root *cli.Command) { + root.AddSubCommand(&cli.Command{ + Name: "__atexit__", + Hidden: true, + OnlyArgsAllowed: true, + Run: func(cmd *cli.Command, args []string) (rc int, err error) { + if len(args) != 0 { + return 1, fmt.Errorf("Usage: __atexit__") + } + return main() + }, + }) +} diff --git a/tools/cmd/tool/main.go b/tools/cmd/tool/main.go index 1fb0839f68d..43d535bc4f0 100644 --- a/tools/cmd/tool/main.go +++ b/tools/cmd/tool/main.go @@ -21,6 +21,7 @@ import ( "kitty/kittens/unicode_input" "kitty/tools/cli" "kitty/tools/cmd/at" + "kitty/tools/cmd/atexit" "kitty/tools/cmd/benchmark" "kitty/tools/cmd/edit_in_kitty" "kitty/tools/cmd/mouse_demo" @@ -109,6 +110,8 @@ func KittyToolEntryPoints(root *cli.Command) { }) // __convert_image__ images.ConvertEntryPoint(root) + // __atexit__ + atexit.EntryPoint(root) // __generate_man_pages__ root.AddSubCommand(&cli.Command{ Name: "__generate_man_pages__",