Skip to content

Commit 443185b

Browse files
committed
v0.4.0
1 parent 8cbdfc8 commit 443185b

File tree

14 files changed

+880
-295
lines changed

14 files changed

+880
-295
lines changed

.github/workflows/build.yml

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
pull_request:
77

88
jobs:
9-
test:
9+
Linux:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v4
@@ -22,8 +22,6 @@ jobs:
2222
pip install pytest pytest-cov toml
2323
pip install -e .
2424
25-
# - name: Run tests
26-
# run: pytest --cov=runce --cov-report=xml
2725
- name: Run tests
2826
run: sh tests/run_tests_with_coverage.sh
2927

@@ -35,5 +33,40 @@ jobs:
3533
echo "Version mismatch!"
3634
exit 1
3735
fi
38-
# - name: Upload coverage to Codecov
39-
# uses: codecov/codecov-action@v3
36+
37+
Windows:
38+
runs-on: windows-latest
39+
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- name: Set up Python
44+
uses: actions/setup-python@v5
45+
with:
46+
python-version: "3.10"
47+
48+
- name: Install dependencies
49+
run: |
50+
python -m pip install --upgrade pip
51+
pip install pytest pytest-cov toml
52+
pip install -e .
53+
54+
# - name: Prep tests
55+
# env:
56+
# TARGET: runce
57+
# run: python tests/remove_emojis.py
58+
59+
# - name: Run tests/test_windows.py
60+
# run: python tests/test_windows.py
61+
# continue-on-error: true
62+
63+
# - name: Run tests/test_utils.py
64+
# run: python tests/test_utils.py
65+
# continue-on-error: true
66+
67+
# - name: Run tests/test_cli.py
68+
# run: python tests/test_cli.py
69+
# continue-on-error: true
70+
71+
- name: Run tests
72+
run: python -m pytest

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ description = "Ensure only one instance of your process runs at a time."
1212
authors = [{name = "Jet-Logic"}]
1313
readme = "README.md"
1414
requires-python = ">=3.8"
15-
license = {text = "MIT"}
1615
classifiers = [
1716
"Operating System :: POSIX :: Linux",
1817
"Development Status :: 4 - Beta",

runce/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
44
A lightweight process manager that guarantees single execution.
55
Ensures each command runs exactly once per unique ID.
6-
7-
License: GPL-3.0
86
"""
97

10-
__version__ = "0.3.0"
8+
__version__ = "0.4.0"

runce/cli.py

Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import signal
21
from argparse import ArgumentParser
32
from shlex import join
43
from sys import stderr, stdout
5-
from os import getpgid, killpg
64
from shutil import copyfileobj
75
from subprocess import Popen, PIPE, run
6+
import sys
87
from typing import Dict, Any
9-
from .utils import check_pid
8+
from .utils import check_pid, filesizepu, kill_pid, tail_bytes, tail_file
109
from .main import Main, flag, arg
11-
1210
from .procdb import ProcessDB as Manager
1311

1412
# from .spawn import Spawn as Manager
@@ -17,7 +15,7 @@
1715
class FormatDict(dict):
1816
def __missing__(self, key: str) -> str:
1917
if key == "pid?":
20-
return f'{self["pid"]}{"" if check_pid(self["pid"]) else "?👻"}'
18+
return f'{self["pid"]}{"" if check_pid(self["pid"]) else "?"}'
2119
elif key == "elapsed":
2220
import time
2321

@@ -27,7 +25,7 @@ def __missing__(self, key: str) -> str:
2725
return self["cmd"]
2826
return join(self["cmd"])
2927
elif key == "pid_status":
30-
return "✅ Live" if check_pid(self["pid"]) else "❌ Gone"
28+
return "Running" if check_pid(self["pid"]) else "Stopped"
3129
raise KeyError(f"No {key!r}")
3230

3331

@@ -40,11 +38,19 @@ def fn(x: Dict[str, Any]) -> str:
4038

4139

4240
def no_record(name):
43-
print(f"🤷‍ No record of {name!r}")
41+
try:
42+
print("🤷‍ ", end="")
43+
except UnicodeEncodeError:
44+
pass
45+
print(f"No record of {name!r}")
4446

4547

4648
def ambiguous(name):
47-
print(f"⁉️ {name!r} is ambiguous")
49+
try:
50+
print("⁉️ ", end="")
51+
except UnicodeEncodeError:
52+
pass
53+
print(f"{name!r} is ambiguous")
4854

4955

5056
class Clean(Main):
@@ -61,7 +67,11 @@ def start(self) -> None:
6167
for d in sp.find_names(self.ids, ambiguous, no_record):
6268
if check_pid(d["pid"]):
6369
continue
64-
print(f"🧹 Cleaning {d['pid']} {d['name']}")
70+
try:
71+
print("🧹 ", end="")
72+
except UnicodeEncodeError:
73+
pass
74+
print(f"Cleaning {d['pid']} {d['name']}")
6575
sp.drop(d)
6676

6777

@@ -81,6 +91,7 @@ def init_argparse(self, argp: ArgumentParser) -> None:
8191

8292
def start(self) -> None:
8393
f = format_prep(self.format)
94+
e = ["✅", "❌"]
8495
for d in Manager().find_names(self.ids, ambiguous, no_record):
8596
print(f(d))
8697

@@ -91,70 +102,92 @@ class Kill(Main):
91102
ids: list[str] = arg("ID", "run ids", nargs="+")
92103
dry_run: bool = flag("dry-run", "dry run (don't actually kill)", default=False)
93104
remove: bool = flag("remove", "remove entry after killing", default=False)
105+
group: bool = flag("group", "kill process group", default=False)
106+
signal: str = flag("signal", "send signal", default=None)
94107

95108
def init_argparse(self, argp: ArgumentParser) -> None:
96109
argp.description = "Kill the process of a run id"
97110
return super().init_argparse(argp)
98111

99112
def start(self) -> None:
113+
_errdef = ["❌", "Error"]
114+
_noproc = ["👻", "No process"]
115+
_killed = ["💀", "Killed"]
116+
signal = int(self.signal) if self.signal else None
100117
sp = Manager()
101118
if self.ids:
102119
for x in sp.find_names(self.ids, ambiguous, no_record):
103-
pref = "❌ Error"
120+
s = _errdef
121+
if self.dry_run:
122+
s = _killed
123+
else:
124+
if check_pid(x["pid"]):
125+
if kill_pid(x["pid"], process_group=self.group):
126+
s = _killed
127+
else:
128+
s = _noproc
104129
try:
105-
pgid = getpgid(x["pid"])
106-
if not self.dry_run:
107-
killpg(pgid, signal.SIGTERM)
108-
pref = "💀 Killed"
109-
except ProcessLookupError:
110-
pref = "👻 No process"
111-
finally:
112-
print(f'{pref} PID={x["pid"]} {x["name"]!r}')
113-
if not self.dry_run and self.remove:
114-
sp.drop(x)
130+
print(f"{s[0]} ", end="")
131+
except UnicodeEncodeError:
132+
pass
133+
print(f'{s[1]} PID={x["pid"]} {x["name"]!r}')
134+
if not self.dry_run and self.remove:
135+
sp.drop(x)
136+
137+
138+
def _tail(n: float, u="", out="", tab=None):
139+
if u:
140+
stdout.buffer.write(tail_bytes(out, int(n)))
141+
elif n > 0:
142+
if sys.platform.startswith("win"):
143+
cmd = [
144+
"powershell",
145+
"-c",
146+
f"Get-Content -Tail {int(n)} '{out}'",
147+
]
148+
else:
149+
cmd = ["tail", "-n", str(int(n)), out]
150+
if tab:
151+
with Popen(cmd, stdout=PIPE).stdout as o:
152+
for line in o:
153+
stdout.buffer.write(b"\t" + line)
154+
else:
155+
run(cmd)
115156

116157

117158
class Tail(Main):
118159
"""Tail process output."""
119160

120161
ids: list[str] = arg("ID", "run ids", nargs="*")
121162
format: str = flag("header", "header format")
122-
lines: int = flag("n", "lines", "how many lines")
163+
lines: str = flag("n", "lines", "how many lines or bytes")
123164
existing: bool = flag(
124165
"x", "only-existing", "only show existing processes", default=False
125166
)
126167
tab: bool = flag("t", "tab", "prefix tab space", default=False)
127168
err: bool = flag("e", "err", "output stderr", default=False)
128-
p_open: str = "📜 "
129-
p_close: str = ""
169+
p_open: str = "=== "
170+
p_close: str = " ==="
130171

131172
def start(self) -> None:
173+
import sys
174+
132175
if self.format == "no":
133176
hf = None
134177
else:
135178
hf = format_prep(self.format or r"{pid?}: {name}")
136-
lines = self.lines or 10
179+
n, u = filesizepu(self.lines or "10")
137180
j = 0
138181
out = "err" if self.err else "out"
139182

140183
for x in Manager().find_names(self.ids, ambiguous, no_record):
141184
if self.existing and not check_pid(x["pid"]):
142185
continue
143186

144-
j > 1 and lines > 0 and print()
187+
j > 1 and n > 0 and print()
145188
if hf:
146189
print(f"{self.p_open}{hf(x)}{self.p_close}", flush=True)
147-
148-
if lines > 0:
149-
# TODO: pythonify
150-
cmd = ["tail", "-n", str(lines), x[out]]
151-
if self.tab:
152-
with Popen(cmd, stdout=PIPE).stdout as o:
153-
for line in o:
154-
stdout.buffer.write(b"\t" + line)
155-
else:
156-
run(cmd)
157-
stdout.flush()
190+
_tail(n, u, x[out], self.tab)
158191
j += 1
159192

160193

@@ -165,11 +198,11 @@ class Run(Main):
165198
tail: int = flag("t", "tail", "tail the output with n lines", default=0)
166199
run_id: str = flag("id", "Unique run identifier", default="")
167200
cwd: str = flag("Working directory for the command")
168-
tail: int = flag(
201+
tail: str = flag(
169202
"t",
170203
"tail",
171204
"Tail the output (n lines). Use `-t -1` to print the entire output",
172-
default=0,
205+
# default=0,
173206
)
174207
overwrite: bool = flag("overwrite", "Overwrite existing entry", default=False)
175208
cmd_after: str = flag("run-after", "Run command after", metavar="command")
@@ -183,24 +216,25 @@ def start(self) -> None:
183216
# Check for existing process first
184217
e = sp.find_name(name) if name else None
185218
if e:
186-
hf = format_prep(r"🚨 Found: PID={pid} ({pid_status}) {name}")
187-
print(hf(e), file=stderr)
219+
s = ["🚨", r"Found: PID={pid} ({pid_status}) {name}"]
188220
else:
189221
# Start new process
190222
e = sp.spawn(
191223
args, name, overwrite=self.overwrite, cwd=self.cwd, split=self.split
192224
)
193-
hf = format_prep("🚀 Started: PID={pid} ({pid_status}) {name}")
194-
print(hf(e), file=stderr)
225+
s = ["🚀", r"Started: PID={pid} ({pid_status}) {name}"]
195226
assert e
227+
try:
228+
print(f"{s[0]} ", end="", file=stderr)
229+
except UnicodeEncodeError:
230+
pass
231+
hf = format_prep(s[1])
232+
print(hf(e), file=stderr, flush=True)
196233

197234
# Handle tail output
198235
if self.tail:
199-
if self.tail < 0:
200-
with open(e["out"], "rb") as f:
201-
copyfileobj(f, stdout.buffer)
202-
elif self.tail > 0:
203-
run(["tail", "-n", str(self.tail), e["out"]])
236+
n, u = filesizepu(self.tail)
237+
_tail(n, u, e["out"])
204238

205239
# Run post-command if specified
206240
if self.cmd_after:
@@ -228,7 +262,7 @@ def start(self) -> None:
228262
else:
229263
f = "{pid_status} {elapsed} {pid}\t{name}, {command}"
230264
print("Status Elapsed PID\tName, Command")
231-
print("─────── ──────── ────── ────────────")
265+
print("------- -------- ------ ------------")
232266
fp = format_prep(f)
233267
for d in Manager().all():
234268
print(fp(d))

0 commit comments

Comments
 (0)