Skip to content

Commit e6a3151

Browse files
committed
better daemonize + pidfile + rust client
1 parent 3c1870c commit e6a3151

File tree

6 files changed

+156
-74
lines changed

6 files changed

+156
-74
lines changed

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,15 @@ cython_debug/
163163

164164
# ignore all .out files generated by jsi runs
165165
**/*.out
166+
167+
# Generated by Cargo
168+
# will have compiled files and executables
169+
**/debug/
170+
**/target/
171+
172+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
173+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
174+
Cargo.lock
175+
176+
# These are backup files generated by rustfmt
177+
**/*.rs.bk

jsi-client-rs/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "jsif"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "just solve it fast - rust client for the jsi daemon"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]

jsi-client-rs/src/main.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use std::env;
2+
use std::io::{Read, Write};
3+
use std::os::unix::net::UnixStream;
4+
use std::path::PathBuf;
5+
use std::process;
6+
use std::time::Instant;
7+
8+
fn get_server_home() -> Option<PathBuf> {
9+
env::var_os("HOME").map(|home| {
10+
let mut path = PathBuf::from(home);
11+
path.push(".jsi");
12+
path.push("daemon");
13+
path
14+
})
15+
}
16+
17+
fn send_command(command: &str) -> Result<(), Box<dyn std::error::Error>> {
18+
let socket_path = get_server_home().unwrap().join("server.sock");
19+
let mut stream = UnixStream::connect(socket_path)?;
20+
21+
// Send the command
22+
stream.write_all(command.as_bytes())?;
23+
stream.flush()?;
24+
25+
// Read the response
26+
let start = Instant::now();
27+
let mut response = String::new();
28+
stream.read_to_string(&mut response)?;
29+
30+
println!("{}", response);
31+
println!("; response time: {:?}", start.elapsed());
32+
Ok(())
33+
}
34+
35+
fn main() {
36+
let args: Vec<String> = env::args().collect();
37+
38+
if args.len() < 2 {
39+
eprintln!("Usage: {} <command>", args[0]);
40+
process::exit(1);
41+
}
42+
43+
let command = args[1..].join(" ");
44+
45+
match send_command(&command) {
46+
Ok(_) => (),
47+
Err(e) => eprintln!("Error: {}", e),
48+
}
49+
}

src/jsi/cli.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -260,22 +260,11 @@ def main(args: list[str] | None = None) -> int:
260260
logger.enable(console=stderr, level=LogLevel.DEBUG)
261261

262262
if config.daemon:
263-
import asyncio
264-
265-
import daemon
266-
267-
from jsi.server import STDERR_PATH, STDOUT_PATH, Server
268-
269-
async def run_server():
270-
server = Server(config)
271-
await server.start()
272-
273-
stdout_file = open(STDOUT_PATH, "w+") # noqa: SIM115
274-
stderr_file = open(STDERR_PATH, "w+") # noqa: SIM115
275-
276-
with daemon.DaemonContext(stdout=stdout_file, stderr=stderr_file):
277-
asyncio.run(run_server())
263+
import jsi.server
278264

265+
# this detaches the server from the current shell,
266+
# this returns immediately, leaving the server running in the background
267+
jsi.server.daemonize(config)
279268
return 0
280269

281270
with timer("load_config"):

src/jsi/server.py

Lines changed: 77 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
1+
"""
2+
A daemon that listens for requests on a unix socket.
3+
4+
Can be started with:
5+
6+
# with a command line interface to parse the config:
7+
jsi [options] --daemon
8+
9+
# or with a default config:
10+
python -m jsi.server
11+
12+
These commands return immediately, as a detached daemon runs in the background.
13+
14+
The daemon:
15+
- checks if there is an existing daemon (as indicated by ~/.jsi/daemon/server.pid)
16+
- it kills the existing daemon if found
17+
- it writes its own pid to ~/.jsi/daemon/server.pid
18+
- it outputs logs to ~/.jsi/daemon/server.{err,out}
19+
- it listens for requests on a unix domain socket (by default ~/.jsi/daemon/server.sock)
20+
- each request is a single line of text, the path to a file to solve
21+
- for each request, it runs the sequence of solvers defined in the config
22+
- it returns the output of the solvers, based on the config
23+
- it runs until terminated by the user or another daemon
24+
"""
25+
126
import asyncio
227
import contextlib
328
import os
429
import signal
5-
import socket
630
import threading
31+
from pathlib import Path
732

833
import daemon # type: ignore
934

@@ -20,17 +45,35 @@
2045
base_commands,
2146
set_input_output,
2247
)
23-
from jsi.utils import pid_exists
48+
from jsi.utils import get_consoles, pid_exists, unexpand_home
2449

25-
SERVER_HOME = os.path.expanduser("~/.jsi/daemon")
26-
SOCKET_PATH = os.path.join(SERVER_HOME, "server.sock")
27-
STDOUT_PATH = os.path.join(SERVER_HOME, "server.out")
28-
STDERR_PATH = os.path.join(SERVER_HOME, "server.err")
29-
PID_PATH = os.path.join(SERVER_HOME, "server.pid")
50+
SERVER_HOME = Path.home() / ".jsi" / "daemon"
51+
SOCKET_PATH = SERVER_HOME / "server.sock"
52+
STDOUT_PATH = SERVER_HOME / "server.out"
53+
STDERR_PATH = SERVER_HOME / "server.err"
54+
PID_PATH = SERVER_HOME / "server.pid"
3055
CONN_BUFFER_SIZE = 1024
3156

32-
# TODO: handle signal.SIGCHLD (received when a child process exits)
33-
# TODO: check if there is an existing daemon
57+
58+
unexpanded_pid = unexpand_home(PID_PATH)
59+
server_usage = f"""[bold white]starting daemon...[/]
60+
61+
- tail logs:
62+
[green]tail -f {unexpand_home(STDERR_PATH)[:-4]}.{{err,out}}[/]
63+
64+
- view pid of running daemon:
65+
[green]cat {unexpanded_pid}[/]
66+
67+
- display useful info about current daemon:
68+
[green]ps -o pid,etime,command -p $(cat {unexpanded_pid})[/]
69+
70+
- terminate daemon (gently, with SIGTERM):
71+
[green]kill $(cat {unexpanded_pid})[/]
72+
73+
- terminate daemon (forcefully, with SIGKILL):
74+
[green]kill -9 $(cat {unexpanded_pid})[/]
75+
76+
(use the commands above to monitor the daemon, this process will exit immediately)"""
3477

3578

3679
class ResultListener:
@@ -57,9 +100,10 @@ def result(self) -> str:
57100

58101

59102
class PIDFile:
60-
def __init__(self, path: str):
103+
def __init__(self, path: Path):
61104
self.path = path
62-
self.pid = os.getpid()
105+
# don't get the pid here, as it may not be the current pid anymore
106+
# by the time we enter the context manager
63107

64108
def __enter__(self):
65109
try:
@@ -70,18 +114,22 @@ def __enter__(self):
70114
if pid_exists(int(other_pid)):
71115
print(f"killing existing daemon ({other_pid=})")
72116
os.kill(int(other_pid), signal.SIGKILL)
117+
118+
else:
119+
print(f"pid file points to dead daemon ({other_pid=})")
73120
except FileNotFoundError:
74121
# pid file doesn't exist, we're good to go
75122
pass
76123

77124
# overwrite the file if it already exists
125+
pid = os.getpid()
78126
with open(self.path, "w") as fd:
79-
fd.write(str(self.pid))
127+
fd.write(str(pid))
80128

81-
print(f"created pid file: {self.path} ({self.pid=})")
129+
print(f"created pid file: {self.path} ({pid=})")
82130
return self.path
83131

84-
def __exit__(self, exc_type, exc_value, traceback):
132+
def __exit__(self, exc_type, exc_value, traceback): # type: ignore
85133
print(f"removing pid file: {self.path}")
86134

87135
# ignore if the file was already removed
@@ -101,10 +149,11 @@ def __init__(self, config: Config):
101149

102150
async def start(self):
103151
server = await asyncio.start_unix_server(
104-
self.handle_client, path=SOCKET_PATH
152+
self.handle_client, path=str(SOCKET_PATH)
105153
)
106154

107155
async with server:
156+
print(f"server started on {unexpand_home(SOCKET_PATH)}")
108157
await server.serve_forever()
109158

110159
async def handle_client(
@@ -153,55 +202,25 @@ def sync_solve(self, file: str) -> str:
153202

154203
return listener.result
155204

156-
# def start(self, detach_process: bool | None = None):
157-
# if not os.path.exists(SERVER_HOME):
158-
# print(f"creating server home: {SERVER_HOME}")
159-
# os.makedirs(SERVER_HOME)
160-
161-
# stdout_file = open(STDOUT_PATH, "w+") # noqa: SIM115
162-
# stderr_file = open(STDERR_PATH, "w+") # noqa: SIM115
163-
164-
# print(f"daemonizing... (`tail -f {STDOUT_PATH[:-4]}.{{err,out}}` to view logs)")
165-
# with daemon.DaemonContext(
166-
# stdout=stdout_file,
167-
# stderr=stderr_file,
168-
# detach_process=detach_process,
169-
# pidfile=PIDFile(PID_PATH),
170-
# ):
171-
# if os.path.exists(SOCKET_PATH):
172-
# print(f"removing existing socket: {SOCKET_PATH}")
173-
# os.remove(SOCKET_PATH)
174-
175-
# print(f"binding socket: {SOCKET_PATH}")
176-
# with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
177-
# server.bind(SOCKET_PATH)
178-
# server.listen(1)
179-
180-
# while True:
181-
# try:
182-
# conn, _ = server.accept()
183-
# with conn:
184-
# try:
185-
# data = conn.recv(CONN_BUFFER_SIZE).decode()
186-
# if not data:
187-
# continue
188-
# print(f"solving: {data}")
189-
# conn.sendall(self.solve(data).encode())
190-
# except ConnectionError as e:
191-
# print(f"connection error: {e}")
192-
# except SystemExit as e:
193-
# print(f"system exit: {e}")
194-
# return e.code
195-
196205

197-
if __name__ == "__main__":
206+
def daemonize(config: Config):
207+
stdout, _ = get_consoles()
208+
stdout.print(server_usage)
198209

199210
async def run_server():
200-
server = Server(Config())
211+
server = Server(config)
201212
await server.start()
202213

203214
stdout_file = open(STDOUT_PATH, "w+") # noqa: SIM115
204215
stderr_file = open(STDERR_PATH, "w+") # noqa: SIM115
205216

206-
with daemon.DaemonContext(stdout=stdout_file, stderr=stderr_file):
217+
with daemon.DaemonContext(
218+
stdout=stdout_file,
219+
stderr=stderr_file,
220+
pidfile=PIDFile(PID_PATH),
221+
):
207222
asyncio.run(run_server())
223+
224+
225+
if __name__ == "__main__":
226+
daemonize(Config())

src/jsi/utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import time
77
from datetime import datetime
88
from enum import Enum
9-
9+
from pathlib import Path
1010

1111
class Closeable:
1212
def close(self) -> None: ...
@@ -32,6 +32,10 @@ def is_terminal(self) -> bool:
3232
return False
3333

3434

35+
def unexpand_home(path: str | Path) -> str:
36+
return str(path).replace(str(Path.home()), "~")
37+
38+
3539
def is_terminal(file: object) -> bool:
3640
return hasattr(file, "isatty") and file.isatty() # type: ignore
3741

0 commit comments

Comments
 (0)