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+
126import asyncio
227import contextlib
328import os
429import signal
5- import socket
630import threading
31+ from pathlib import Path
732
833import daemon # type: ignore
934
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"
3055CONN_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
3679class ResultListener :
@@ -57,9 +100,10 @@ def result(self) -> str:
57100
58101
59102class 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 ())
0 commit comments