From eebb226b210b396258d2c43b4ffe9c85dcf6afe0 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Mon, 3 Aug 2026 09:46:44 +1000 Subject: [PATCH] Use worker threads instead of subprocesses, bump to 2026.8.3 Convert each Worker from a subprocess to a thread running the tool in a ThreadPoolExecutor, so eris can run free-threaded under CPython 3.14t. Bump fill3 and lscolors deps to 2026.8.3 for the GIL-free cwcwidth chain. --- eris/__main__.py | 5 +--- eris/worker.py | 65 ++++++++------------------------------------ pyproject.toml | 6 ++-- tests/worker_test.py | 3 -- uv.lock | 27 +++++++++--------- 5 files changed, 28 insertions(+), 78 deletions(-) diff --git a/eris/__main__.py b/eris/__main__.py index 3c52622..82a1ad9 100755 --- a/eris/__main__.py +++ b/eris/__main__.py @@ -61,7 +61,7 @@ Options: -h, --help Show the full help. -i, --info Show information about the available tools. -a, --install-all-tools Install all tools. - -w COUNT, --workers=COUNT The number of processes working in parallel. + -w COUNT, --workers=COUNT The number of threads working in parallel. By default it is the number of cpus minus 1. -e "COMMAND", --editor="COMMAND" The command used to start the editor, in the *edit command. It may contain options. @@ -1266,9 +1266,6 @@ def inotify_watches_exceeded(): def entry_point(): - if "--worker" in sys.argv: - worker.main() - return (root_path, worker_count, editor_command, theme, compression, use_window) = check_arguments() manage_cache(root_path) diff --git a/eris/worker.py b/eris/worker.py index 8cc3966..689ada1 100755 --- a/eris/worker.py +++ b/eris/worker.py @@ -1,8 +1,6 @@ import asyncio -import contextlib -import os -import signal +import concurrent.futures import sys import fill3 @@ -11,12 +9,6 @@ import eris.tools as tools import eris.paged_list -def worker_command(): - if getattr(sys, "frozen", False): - return [sys.executable, "--worker"] - return [sys.executable, os.path.abspath(sys.argv[0]), "--worker"] - - class Worker: AUTOSAVE_MESSAGE = "Auto-saving…" @@ -25,33 +17,20 @@ class Worker: def __init__(self, compression): self.compression = compression self.result = None - self.process = None - self.child_pgid = None + self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) - async def create_process(self, command=None): - create = asyncio.create_subprocess_exec( - *(command or worker_command()), stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - preexec_fn=os.setsid) - self.process = await create - pid_line = await self.process.stdout.readline() - self.child_pgid = int(pid_line.strip()) - os.setpriority(os.PRIO_PGRP, self.child_pgid, 19) - self.process.stdin.write(f"{self.compression}\n".encode("utf-8")) + def _run_job(self, path, tool): + result = tools.Result(path, tool) + result.compression = self.compression + status, text = tools.run_tool_no_error(path, tool) + result.result = make_result_widget(text, result, self.compression) + return status async def run_tool(self, path, tool): - while True: - self.process.stdin.write(f"{tool.__qualname__}\n{path}\n" - .encode("utf-8", "surrogatepass")) - data = await self.process.stdout.readline() - if data == b"": - await self.create_process() - else: - break - return tools.Status(int(data)) + return await asyncio.get_running_loop().run_in_executor( + self._executor, self._run_job, path, tool) async def job_runner(self, screen, summary, log, jobs_added_event): - await self.create_process() while True: await jobs_added_event.wait() while True: @@ -76,9 +55,7 @@ class Worker: jobs_added_event.clear() def kill(self): - if self.child_pgid is not None: - with contextlib.suppress(ProcessLookupError): - os.killpg(self.child_pgid, signal.SIGKILL) + self._executor.shutdown(wait=False, cancel_futures=True) def make_result_widget(text, result, compression): @@ -89,23 +66,3 @@ def make_result_widget(text, result, compression): appearance, result.get_pages_dir(), page_size, cache_size=2, exist_ok=True, open_func=tools.compression_open_func(compression)) return fill3.Fixed(appearance) - - -def main(): - print(os.getpgid(os.getpid()), flush=True) - compression = input() - try: - while True: - tool_name, path = input(), input() - tool = getattr(tools, tool_name) - result = tools.Result(path, tool) - result.compression = compression - status, text = tools.run_tool_no_error(path, tool) - result.result = make_result_widget(text, result, compression) - print(status.value, flush=True) - except Exception: - tools.log_error() - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 07e6fcb..f9a2c46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ PYO3_USE_ABI3_FORWARD_COMPATIBILITY = { value = "1" } [project] name = "eris" -version = "2026.7.28" +version = "2026.8.3" description = "Eris maintains an up-to-date set of reports for every file in a codebase." readme = "README.md" authors = [ @@ -15,8 +15,8 @@ authors = [ requires-python = ">=3.14" dependencies = [ "docopt-ng==0.9.0", - "fill3 @ git+https://forgejo.tail556789.ts.net/ahamilton/fill3.git@2026.7.28", - "lscolors @ git+https://forgejo.tail556789.ts.net/ahamilton/lscolors.git@2026.7.27", + "fill3 @ git+https://forgejo.tail556789.ts.net/ahamilton/fill3.git@2026.8.3", + "lscolors @ git+https://forgejo.tail556789.ts.net/ahamilton/lscolors.git@2026.8.3", "pexpect==4.9.0", "pillow==12.3.0", "pygments==2.20.0", diff --git a/tests/worker_test.py b/tests/worker_test.py index 451cdf3..c9cad5d 100755 --- a/tests/worker_test.py +++ b/tests/worker_test.py @@ -2,7 +2,6 @@ import asyncio import os import shutil -import sys import tempfile import unittest @@ -29,8 +28,6 @@ class WorkerTestCase(unittest.TestCase): loop = asyncio.new_event_loop() compression = "none" self.worker_ = worker.Worker(compression) - command = [sys.executable, "-m", "eris.worker", "--worker"] - loop.run_until_complete(self.worker_.create_process(command=command)) future = self.worker_.run_tool("foo", tools.metadata) status = loop.run_until_complete(future) self.assertEqual(status, tools.Status.ok) diff --git a/uv.lock b/uv.lock index 23c7d86..061599b 100644 --- a/uv.lock +++ b/uv.lock @@ -25,9 +25,8 @@ wheels = [ [[package]] name = "cwcwidth" -version = "0.1.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/17/aadd0c6190dca91aa27c4d7e84d69d30fdf4966e7764247cdc395f8fe7d9/cwcwidth-0.1.5.tar.gz", hash = "sha256:2c840e7d85f6de45c45986b416d79312c91882e1121b78d4c347e49c4238c09d", size = 11071, upload-time = "2021-10-10T16:46:56.825Z" } +version = "0.1.12" +source = { git = "https://forgejo.tail556789.ts.net/ahamilton/cwcwidth.git?rev=v0.1.12#6ac7d0e3c70535a454094e5e7e51e637d05a29c9" } [[package]] name = "docopt-ng" @@ -40,7 +39,7 @@ wheels = [ [[package]] name = "eris" -version = "2026.7.28" +version = "2026.8.3" source = { editable = "." } dependencies = [ { name = "docopt-ng" }, @@ -61,8 +60,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "docopt-ng", specifier = "==0.9.0" }, - { name = "fill3", git = "https://forgejo.tail556789.ts.net/ahamilton/fill3.git?rev=2026.7.28" }, - { name = "lscolors", git = "https://forgejo.tail556789.ts.net/ahamilton/lscolors.git?rev=2026.7.27" }, + { name = "fill3", git = "https://forgejo.tail556789.ts.net/ahamilton/fill3.git?rev=2026.8.3" }, + { name = "lscolors", git = "https://forgejo.tail556789.ts.net/ahamilton/lscolors.git?rev=2026.8.3" }, { name = "pexpect", specifier = "==4.9.0" }, { name = "pillow", specifier = "==12.3.0" }, { name = "pygments", specifier = "==2.20.0" }, @@ -77,8 +76,8 @@ dev = [ [[package]] name = "fill3" -version = "2026.7.28" -source = { git = "https://forgejo.tail556789.ts.net/ahamilton/fill3.git?rev=2026.7.28#faacf3614fa458d311b2f5ab87653f6385e74ee6" } +version = "2026.8.3" +source = { git = "https://forgejo.tail556789.ts.net/ahamilton/fill3.git?rev=2026.8.3#ed3d0923ea26fa7dcdaa1639f5f8493458a84a0c" } dependencies = [ { name = "termstr" }, { name = "textgrid" }, @@ -95,8 +94,8 @@ wheels = [ [[package]] name = "lscolors" -version = "2026.7.27" -source = { git = "https://forgejo.tail556789.ts.net/ahamilton/lscolors.git?rev=2026.7.27#288d88c9aea9deda54662d6fd5fb0d4dfe9e80fe" } +version = "2026.8.3" +source = { git = "https://forgejo.tail556789.ts.net/ahamilton/lscolors.git?rev=2026.8.3#ff650469c5c2d3cfeeeacb3f17afba2ee57f97ba" } dependencies = [ { name = "termstr" }, ] @@ -255,13 +254,13 @@ wheels = [ [[package]] name = "termstr" -version = "2026.7.27" -source = { git = "https://forgejo.tail556789.ts.net/ahamilton/termstr.git?rev=2026.7.27#7d7a8e9ecd52774dc474c6cad08f26f06b274889" } +version = "2026.8.3" +source = { git = "https://forgejo.tail556789.ts.net/ahamilton/termstr.git?rev=2026.8.3#12cf0e42691fb241932bdfbb17a7e794978a45cf" } dependencies = [ { name = "cwcwidth" }, ] [[package]] name = "textgrid" -version = "0.1.0" -source = { git = "https://forgejo.tail556789.ts.net/ahamilton/textgrid-rust.git?rev=2026.7.28#49125c7a38cf2beae45579bb4dc79b8215c1c5ab" } +version = "2026.8.3" +source = { git = "https://forgejo.tail556789.ts.net/ahamilton/textgrid-rust.git?rev=2026.8.3#9d2af4e58f689502e6f7ff47dd1a45c01a76f9c0" }