Skip to content

Commit b41b8c3

Browse files
jet-logicjet-logic
authored andcommitted
Now using SQLite
1 parent a847f78 commit b41b8c3

File tree

9 files changed

+151
-280
lines changed

9 files changed

+151
-280
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,23 +176,23 @@ runce restart my-unique-task
176176
runce run --id api-server -- python api.py
177177
```
178178

179-
### 2. Checking Running Processes
179+
### 2. Checking Live Processes
180180

181181
```bash
182182
$ runce list
183183
PID NAME STATUS ELAPSED COMMAND
184-
1234 api-server ✅ Running 01:23:45 python api.py
185-
5678 worker ❌ Stopped 00:45:30 python worker.py
184+
1234 api-server ✅ Live 01:23:45 python api.py
185+
5678 worker ❌ Gone 00:45:30 python worker.py
186186
```
187187

188188
### 3. Preventing Duplicates
189189

190190
```bash
191191
$ runce run --id daily-job -- python daily.py
192-
🚀 Started: PID:5678(✅ Running) daily-job
192+
🚀 Started: PID:5678(✅ Live) daily-job
193193

194194
$ runce run --id daily-job -- python daily.py
195-
🚨 Already running: PID:5678(✅ Running) daily-job
195+
🚨 Already running: PID:5678(✅ Live) daily-job
196196
```
197197

198198
## Formats
@@ -201,7 +201,7 @@ The `-f` / `--format` option in the `ls` and `status` commands allows you to cus
201201

202202
- `{pid}`: Process ID
203203
- `{name}`: Run ID / Name
204-
- `{pid_status}`: Process status ("✅ Running" or "👻 Absent")
204+
- `{pid_status}`: Process status ("✅ Live" or "👻 Gone")
205205
- `{elapsed}`: Elapsed time
206206
- `{command}`: The command being executed
207207

runce/cli.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import signal
22
from argparse import ArgumentParser
3-
from subprocess import Popen, PIPE, run
4-
from typing import List, Dict, Any, Optional
3+
from shlex import join
54
from sys import stderr, stdout
65
from os import getpgid, killpg
7-
from shlex import join
86
from shutil import copyfileobj
9-
from .spawn import Spawn
7+
from subprocess import Popen, PIPE, run
8+
from typing import Dict, Any
109
from .utils import check_pid
1110
from .main import Main, flag, arg
1211

12+
from .procdb import ProcessDB as Manager
13+
14+
# from .spawn import Spawn as Manager
15+
1316

1417
class FormatDict(dict):
1518
def __missing__(self, key: str) -> str:
@@ -24,12 +27,9 @@ def __missing__(self, key: str) -> str:
2427
return self["cmd"]
2528
return join(self["cmd"])
2629
elif key == "pid_status":
27-
return "✅ Running" if check_pid(self["pid"]) else "👻 Absent"
30+
return "✅ Live" if check_pid(self["pid"]) else "❌ Gone"
2831
raise KeyError(f"No {key!r}")
2932

30-
def __call__(self, *args: Any, **kwds: Any) -> Any:
31-
return super().__call__(*args, **kwds)
32-
3333

3434
def format_prep(f: str):
3535

@@ -57,7 +57,7 @@ def add_arguments(self, argp: ArgumentParser) -> None:
5757
return super().add_arguments(argp)
5858

5959
def start(self) -> None:
60-
sp = Spawn()
60+
sp = Manager()
6161
for d in sp.find_names(self.ids, ambiguous, no_record):
6262
if check_pid(d["pid"]):
6363
continue
@@ -81,7 +81,7 @@ def init_argparse(self, argp: ArgumentParser) -> None:
8181

8282
def start(self) -> None:
8383
f = format_prep(self.format)
84-
for d in Spawn().find_names(self.ids, ambiguous, no_record):
84+
for d in Manager().find_names(self.ids, ambiguous, no_record):
8585
print(f(d))
8686

8787

@@ -97,7 +97,7 @@ def init_argparse(self, argp: ArgumentParser) -> None:
9797
return super().init_argparse(argp)
9898

9999
def start(self) -> None:
100-
sp = Spawn()
100+
sp = Manager()
101101
if self.ids:
102102
for x in sp.find_names(self.ids, ambiguous, no_record):
103103
pref = "❌ Error"
@@ -137,7 +137,7 @@ def start(self) -> None:
137137
j = 0
138138
out = "err" if self.err else "out"
139139

140-
for x in Spawn().find_names(self.ids, ambiguous, no_record):
140+
for x in Manager().find_names(self.ids, ambiguous, no_record):
141141
if self.existing and not check_pid(x["pid"]):
142142
continue
143143

@@ -173,7 +173,7 @@ class Run(Main):
173173
def start(self) -> None:
174174
args = self.args
175175
name = self.run_id # or " ".join(x for x in args)
176-
sp = Spawn()
176+
sp = Manager()
177177

178178
# Check for existing process first
179179
e = sp.find_name(name) if name else None
@@ -209,7 +209,7 @@ class Ls(Main):
209209
format: str = flag(
210210
"f",
211211
"format of entry line",
212-
default="{pid}\t{name}\t{pid_status}\t{elapsed}\t{command}",
212+
default="{pid_status} {elapsed} {pid}\t{name}, {command}",
213213
)
214214

215215
def init_argparse(self, argp: ArgumentParser) -> None:
@@ -218,9 +218,9 @@ def init_argparse(self, argp: ArgumentParser) -> None:
218218

219219
def start(self) -> None:
220220
f = format_prep(self.format)
221-
print("PID\tName\tStatus\tElapsed\tCommand")
222-
print("───\t────\t──────\t───────\t───────")
223-
for d in Spawn().all():
221+
print("Status Elapsed PID\tName, Command")
222+
print("─────── ──────── ────── ────────────")
223+
for d in Manager().all():
224224
print(f(d))
225225

226226

@@ -235,13 +235,13 @@ def init_argparse(self, argp: ArgumentParser) -> None:
235235
return super().init_argparse(argp)
236236

237237
def start(self) -> None:
238-
sp = Spawn()
238+
sp = Manager()
239239
if self.ids:
240240
for proc in sp.find_names(self.ids, ambiguous, no_record):
241241
# First kill existing process
242242
Kill().main(["--remove", proc["name"]])
243243
# Then restart with same parameters
244-
Run().main(["--id", proc["name"], "-t", self.tail, *proc["cmd"]])
244+
Run().main(["--id", proc["name"], "-t", self.tail, "--", *proc["cmd"]])
245245

246246

247247
class App(Main):

runce/procdb.py

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# db.py
21
import sqlite3
32
from pathlib import Path
43
from .spawn import Spawn
@@ -79,39 +78,6 @@ def all(self):
7978
"started": row["started"],
8079
}
8180

82-
# def get_process(self, name: str) -> Optional[Dict]:
83-
# """Fetch a process by name."""
84-
# with sqlite3.connect(self.db_path) as conn:
85-
# cursor = conn.execute(
86-
# "SELECT * FROM processes WHERE name = ? AND is_active = 1",
87-
# (name,),
88-
# )
89-
# row = cursor.fetchone()
90-
# if row:
91-
# return {
92-
# "id": row[0],
93-
# "pid": row[1],
94-
# "name": row[2],
95-
# "cmd": eval(row[3]), # Convert JSON string back to list
96-
# "stdout_path": row[4],
97-
# "stderr_path": row[5],
98-
# "started": row[6],
99-
# }
100-
# return None
101-
102-
# def kill_process(self, name: str) -> bool:
103-
# """Mark a process as inactive (killed)."""
104-
# with sqlite3.connect(self.db_path) as conn:
105-
# conn.execute(
106-
# "UPDATE processes SET is_active = 0 WHERE name = ?",
107-
# (name,),
108-
# )
109-
# return conn.total_changes > 0
110-
111-
# def cleanup(self) -> None:
112-
# """Remove all inactive processes."""
113-
# with sqlite3.connect(self.db_path) as conn:
114-
# conn.execute("DELETE FROM processes WHERE is_active = 0")
11581
def find_uuid(self, uuid: str):
11682
"""Find a process by uuid"""
11783
with self.connect() as conn:
@@ -128,3 +94,12 @@ def find_uuid(self, uuid: str):
12894
"err": row["err"],
12995
"started": row["started"],
13096
}
97+
98+
def drop(self, entry: dict[str, object], clean_up=True):
99+
with self.connect() as conn:
100+
conn.cursor().execute(
101+
"DELETE FROM processes WHERE uuid = ?", (entry["uuid"],)
102+
)
103+
# print(f"Row with ID {entry["id"]} deleted successfully. {entry["name"]}")
104+
super().drop(entry, clean_up)
105+
assert self.find_uuid(entry["uuid"]) is None

runce/spawn.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,9 @@ def spawn(
9393

9494
process_info["base_name"] = base_name
9595
process_info["pid"] = Popen(cmd, **po_kwa).pid
96-
# print("PI", process_info)
97-
return self.add_process(process_info)
96+
x = self.add_process(process_info)
97+
# print("PI", x)
98+
return x
9899

99100
def add_process(self, process_info: dict[str, object]):
100101
"""Insert a new process record."""

tests/test_cli.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from pathlib import Path
2+
from unittest import TestCase, main
3+
from runce.procdb import ProcessDB
4+
from runce.spawn import Spawn
5+
from runce.utils import slugify, get_base_name, look
6+
from subprocess import PIPE, run
7+
import re
8+
9+
10+
class TestUtils(TestCase):
11+
12+
def run_runce(self, *args, stdout_only=False):
13+
"""Helper to run python -m runce with stderr capture"""
14+
cmd = ["python", "-m", "runce", *args]
15+
print("RUN:", *cmd)
16+
result = run(cmd, stdout=PIPE, stderr=PIPE, text=True)
17+
o = result.stdout + result.stderr
18+
print(o)
19+
# if stdout_only:
20+
# return result.stdout
21+
# Combine stdout and stderr for verification
22+
return o
23+
24+
def test_split(self):
25+
o = self.run_runce(
26+
"run", "--split", "--", "bash", "-c", "echo -n 123; echo -n 456 >&2"
27+
)
28+
m = re.search(r"(?mx) \W+ Started: \W+ [^\)]+ \s+ \( [^\)]+ \) \s+ (.+)", o)
29+
self.assertTrue(m)
30+
n = m.group(1)
31+
self.assertTrue(n)
32+
o = self.run_runce("tail", "--header", "no", n)
33+
# print(n)
34+
self.assertEqual(o, "123")
35+
o = self.run_runce("tail", "--err", "--header", "no", n)
36+
self.assertEqual(o, "456")
37+
self.run_runce("kill", n)
38+
self.run_runce("clean", n)
39+
40+
def test_cli(self):
41+
o = self.run_runce("run", "--id", "apple", "--", "bash", "-c", "sleep 10")
42+
self.assertRegex(o, r"(?xim) \W+ started: \W+ .+ \W+ apple \W+")
43+
44+
o = self.run_runce(
45+
"run",
46+
"--split",
47+
"--id",
48+
"banana",
49+
"--",
50+
"bash",
51+
"-c",
52+
'for ((i=0; i<10; i++)); do echo "banana $i" >&2; sleep 2; done',
53+
)
54+
self.assertRegex(o, r"(?xim) \W+ started: \W+ .+ \W+ banana \W+")
55+
56+
o = self.run_runce(
57+
"run",
58+
"--id",
59+
"pineapple",
60+
"--",
61+
"bash",
62+
"-c",
63+
'for ((i=0; i<10; i++)); do echo "pineapple $i"; sleep 2; done',
64+
)
65+
self.assertRegex(o, r"(?xim) \W+ started: \W+ .+ \W+ pineapple \W+")
66+
67+
for x in self.run_runce("status").strip().splitlines():
68+
self.assertRegex(
69+
x, r"(?xi) \W+ live \W+ .+ \W+ (?:apple|pineapple|banana) \W+"
70+
)
71+
o = self.run_runce("tail", "pineapple")
72+
self.assertRegex(o, r"(?xim) \W+ pineapple \W+ \d+ \W+")
73+
74+
o = self.run_runce("tail", "--header", "no", "banana")
75+
self.assertEqual(o, "")
76+
77+
# o = self.run_runce("tail", "--err", "banana")
78+
79+
o = self.run_runce("run", "--id", "banana", "--", "bash", "-c", "sleep 10")
80+
self.assertRegex(o, r"(?xim) \W+ found \W+ .+ \W+ banana \W+")
81+
82+
o = self.run_runce("kill", "app")
83+
self.assertRegex(o, r"(?xim) \W+ app \W+ is \W+ ambiguous")
84+
85+
o = self.run_runce("kill", "apple")
86+
self.assertRegex(o, r"(?xim) killed .+ \W+ apple \W+")
87+
88+
for x in self.run_runce("kill", "lemon", "banana").strip().splitlines():
89+
self.assertRegex(
90+
x,
91+
r"(?xim) \W+ no \s+ record .+ \W+ lemon \W+ | \W+ killed .+ \W+ banana \W+",
92+
)
93+
o = self.run_runce("restart", "banana")
94+
# self.assertRegex()
95+
96+
for x in self.run_runce("status").strip().splitlines():
97+
self.assertRegex(
98+
x,
99+
r"(?xi) \W+ live \W+ .+ \W+ (?:pineapple|banana) | gone \W+ .+ \W+ apple \W+",
100+
)
101+
pass
102+
103+
self.assertRegex(
104+
self.run_runce("clean", "apple"), r"(?xim)^ \W+ Cleaning \W+ .+ \W+ apple"
105+
)
106+
self.run_runce("list")
107+
for x in self.run_runce("kill", "--remove", "pi", "b").strip().splitlines():
108+
self.assertRegex(x, r"(?xi) killed .+ \W+ (?:pineapple|banana) \W+")
109+
110+
111+
if __name__ == "__main__":
112+
main()

tests/test_extra.py

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)