cubething_psync 0.2.1__tar.gz → 0.2.1.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {cubething_psync-0.2.1/src/cubething_psync.egg-info → cubething_psync-0.2.1.dev2}/PKG-INFO +1 -1
  2. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/pyproject.toml +1 -1
  3. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/client/main.py +1 -1
  4. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2/src/cubething_psync.egg-info}/PKG-INFO +1 -1
  5. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/SOURCES.txt +1 -0
  6. cubething_psync-0.2.1.dev2/src/server/args.py +69 -0
  7. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/server/main.py +72 -71
  8. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/LICENSE +0 -0
  9. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/README.md +0 -0
  10. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/setup.cfg +0 -0
  11. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/client/__init__.py +0 -0
  12. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/client/__main__.py +0 -0
  13. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/client/args.py +0 -0
  14. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/common/__init__.py +0 -0
  15. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/common/data.py +0 -0
  16. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/common/log.py +0 -0
  17. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/dependency_links.txt +0 -0
  18. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/entry_points.txt +0 -0
  19. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/requires.txt +0 -0
  20. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/top_level.txt +0 -0
  21. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/server/__init__.py +0 -0
  22. {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/server/__main__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cubething_psync
3
- Version: 0.2.1
3
+ Version: 0.2.1.dev2
4
4
  Summary: Simple project synchronization tool.
5
5
  Author-email: ada mandala <ada@cubething.dev>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cubething_psync"
3
- version = "0.2.1"
3
+ version = "0.2.1.dev2"
4
4
  description = "Simple project synchronization tool."
5
5
  authors = [{name = "ada mandala", email="ada@cubething.dev"}]
6
6
  readme = "README.md"
@@ -121,7 +121,7 @@ class PsyncClient:
121
121
  case LogResp():
122
122
  print(resp.msg, end="")
123
123
  case ErrorResp():
124
- logging.error(resp.msg)
124
+ logging.error(f"Received server error: {resp.msg}")
125
125
  await ws.close()
126
126
  exit(1)
127
127
  case ExitResp():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cubething_psync
3
- Version: 0.2.1
3
+ Version: 0.2.1.dev2
4
4
  Summary: Simple project synchronization tool.
5
5
  Author-email: ada mandala <ada@cubething.dev>
6
6
  License-Expression: MIT
@@ -16,4 +16,5 @@ src/cubething_psync.egg-info/requires.txt
16
16
  src/cubething_psync.egg-info/top_level.txt
17
17
  src/server/__init__.py
18
18
  src/server/__main__.py
19
+ src/server/args.py
19
20
  src/server/main.py
@@ -0,0 +1,69 @@
1
+ import argparse
2
+ from dataclasses import dataclass
3
+ from os import environ
4
+ from pathlib import Path
5
+
6
+ SSL_CERT_PATH: str = environ.get("SSL_CERT_PATH", "~/.local/share/psync/cert.pem")
7
+ SSL_KEY_PATH: str = environ.get("SSL_KEY_PATH", "~/.local/share/psync/key.pem")
8
+ PSYNC_HOST: str = environ.get("PSYNC_SERVER_IP", "0.0.0.0")
9
+ PSYNC_PORT: str = environ.get("PSYNC_SERVER_PORT", "5000")
10
+ PSYNC_ORIGINS: str = environ.get("PSYNC_ORIGINS", "localhost 127.0.0.1")
11
+ PSYNC_LOG: str = environ.get("PSYNC_LOG", "INFO").upper()
12
+ PSYNC_USER: str | None = environ.get("PSYNC_USER", None)
13
+
14
+
15
+ @dataclass
16
+ class Args:
17
+ use_base_env: bool
18
+ host: str
19
+ port: str
20
+ origins: list[str]
21
+ user: str | None
22
+ cert_path: Path
23
+ key_path: Path
24
+
25
+
26
+ parser = argparse.ArgumentParser(
27
+ prog="psync-server",
28
+ usage="""\
29
+ Server for project syncrhonization.
30
+
31
+ In addition to the options below, the client is configurable through environment
32
+ variables.
33
+
34
+ SSL_CERT_PATH - Path to SSL cert
35
+ Default: ./cert.pem
36
+ SSL_KEY_PATH - Path to SSL key
37
+ Default: ./key.pem
38
+ PSYNC_SERVER_IP - IP address on which to listen
39
+ Default: 0.0.0.0
40
+ PSYNC_SERVER_PORT - Port on which to listen
41
+ Default: 5000
42
+ PSYNC_ORIGINS - Space-separated list of accepted incoming IP addresses
43
+ Default: "127.0.0.1 localhost"
44
+ PSYNC_LOG - Log level
45
+ Default: "INFO"
46
+ PSYNC_USER - User to run the synced executables. Try not to use root.
47
+ Default: None (current user)
48
+ """,
49
+ )
50
+ _action = parser.add_argument(
51
+ "--use-base-env",
52
+ "-E",
53
+ help="Use the current environment in addition to the requested values.",
54
+ action="store_true",
55
+ )
56
+
57
+
58
+ def parse_args() -> Args:
59
+ args = vars(parser.parse_args())
60
+ return Args(
61
+ use_base_env=args["use_base_env"],
62
+ host=PSYNC_HOST,
63
+ port=PSYNC_PORT,
64
+ origins=PSYNC_ORIGINS.split(),
65
+ user=PSYNC_USER,
66
+ cert_path=Path(SSL_CERT_PATH).expanduser(),
67
+ key_path=Path(SSL_KEY_PATH).expanduser(),
68
+ )
69
+ print(args)
@@ -2,13 +2,13 @@
2
2
  psync server
3
3
  """
4
4
 
5
+ from pprint import PrettyPrinter
6
+ from dataclasses import dataclass
7
+ from os import environ
5
8
  import asyncio
6
- from asyncio.subprocess import Process
7
9
  from asyncio.tasks import Task
10
+ from asyncio.subprocess import Process
8
11
  from collections.abc import Awaitable
9
- import functools
10
- import operator
11
- from os import environ
12
12
  from os.path import basename
13
13
  import pathlib
14
14
  import signal
@@ -34,13 +34,19 @@ from common.data import (
34
34
  )
35
35
  import logging
36
36
  from common.log import InterceptHandler
37
+ from server.args import (
38
+ PSYNC_LOG,
39
+ Args,
40
+ parse_args,
41
+ )
42
+
43
+ pprint = PrettyPrinter().pformat
37
44
 
38
45
 
39
- SSL_CERT_PATH: str = environ.get("SSL_CERT_PATH", "./cert.pem")
40
- SSL_KEY_PATH: str = environ.get("SSL_KEY_PATH", "./key.pem")
41
- PSYNC_HOST: str = environ.get("PSYNC_SERVER_IP", "0.0.0.0")
42
- PSYNC_PORT: str = environ.get("PSYNC_SERVER_PORT", "5000")
43
- PSYNC_ORIGINS: str = environ.get("PSYNC_ORIGINS", "localhost 127.0.0.1")
46
+ @dataclass
47
+ class PTask:
48
+ task: Task[None]
49
+ process: Process
44
50
 
45
51
 
46
52
  class PsyncServer:
@@ -58,32 +64,23 @@ class PsyncServer:
58
64
  Default: "./cert.pem"
59
65
  SSL_KEY_PATH: Path to the SSL key file.
60
66
  Default: "./key.pem"
61
- PSYNC_LOG_LEVEL: Log level. Should match `logger`_ log levels.
62
- Default: "INFO"
63
67
 
64
68
  .. _logger: https://docs.python.org/3/library/logging.html#logging-levels
65
69
  """
66
70
 
67
- __host: str = PSYNC_HOST
68
- """Local host for the server."""
69
-
70
- __port: int = int(PSYNC_PORT)
71
- """Exposed port for websocket connection."""
71
+ __args: Args
72
72
 
73
- __origins: list[str] = (PSYNC_ORIGINS).split()
74
- """Allowed origins for websocket connections."""
75
-
76
- __sessions: dict[str, Process] = {}
77
- """Active sessions. A dict of IP addresses and the running PID. IP
78
- addresses _must_ match those in origins."""
79
-
80
- __tasks: dict[str, Task[None]] = {}
73
+ __tasks: dict[str, PTask] = {}
81
74
  """Active sessions. A dict of IP addresses and the running log task. IP
82
75
  addresses _must_ match those in origins."""
83
76
 
84
77
  __coroutine: Task[None] | None = None
85
78
  """The main coroutine for this server."""
86
79
 
80
+ def __init__(self, args: Args):
81
+ logging.debug(pprint(args))
82
+ self.__args = args
83
+
87
84
  def __get_host(self, ws: ServerConnection) -> str:
88
85
  addrs: tuple[str, str] = ws.remote_address # pyright: ignore[reportAny]
89
86
  (host, _port) = addrs
@@ -95,22 +92,23 @@ class PsyncServer:
95
92
  """
96
93
  ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
97
94
  ssl_ctx.load_cert_chain(
98
- pathlib.Path(SSL_CERT_PATH).expanduser(),
99
- pathlib.Path(SSL_KEY_PATH).expanduser(),
95
+ pathlib.Path(self.__args.cert_path).expanduser(),
96
+ pathlib.Path(self.__args.key_path).expanduser(),
100
97
  )
101
- print(ssl_ctx.get_ca_certs())
98
+ logging.debug(pprint(ssl_ctx.get_ca_certs()))
102
99
  server = await serve(
103
100
  (self.__handle()),
104
- self.__host,
105
- self.__port,
101
+ self.__args.host,
102
+ self.__args.port,
106
103
  process_request=self.__process_request(),
107
104
  ssl=ssl_ctx,
108
105
  )
109
106
  self.__coroutine = asyncio.create_task(server.serve_forever())
110
107
  try:
111
108
  await self.__coroutine
112
- except RuntimeError:
109
+ except RuntimeError as e:
113
110
  # 'event loop stopped before Future completed'
111
+ logging.info(f"Got error {e}")
114
112
  pass
115
113
 
116
114
  def __process_request(
@@ -119,17 +117,13 @@ class PsyncServer:
119
117
  def inner(ws: ServerConnection, _req: Request) -> Response | None:
120
118
  addrs: tuple[str, str] = ws.remote_address # pyright: ignore[reportAny]
121
119
  (host, _port) = addrs
122
- if host not in self.__origins:
120
+ if host not in self.__args.origins:
123
121
  return ws.respond(400, "Client address not recognized.")
124
122
 
125
123
  return inner
126
124
 
127
125
  async def __end_session(self, ws: ServerConnection):
128
126
  host = self.__get_host(ws)
129
- try:
130
- _ = self.__sessions.pop(host)
131
- except Exception:
132
- pass
133
127
  try:
134
128
  _ = self.__tasks.pop(host)
135
129
  except Exception:
@@ -160,6 +154,7 @@ class PsyncServer:
160
154
  try:
161
155
  req = deserialize(msg)
162
156
  except ValueError as e:
157
+ logging.error(e)
163
158
  await ws.send(serialize(ErrorResp(f"{e}")))
164
159
  continue
165
160
 
@@ -181,35 +176,41 @@ class PsyncServer:
181
176
 
182
177
  async def __open(self, req: OpenReq, ws: ServerConnection):
183
178
  host = self.__get_host(ws)
184
- if self.__sessions.get(host) is not None:
185
- self.__sessions[host]
186
- resp = ErrorResp(msg="Process already open for this client.")
187
- await ws.send(serialize(resp))
188
- else:
189
- path = pathlib.Path.expanduser(req.path).resolve()
190
- args = [str(path), *req.args]
191
- env = {"PYTHONUNBUFFERED": "1", **req.env}
192
- reduced: str = functools.reduce(
193
- operator.add, map(lambda x: (f"{x[0]}={x[1]}"), env.items())
179
+ if self.__tasks.get(host) is not None:
180
+ ptask = self.__tasks[host]
181
+ logging.warn(
182
+ f"Cancelling previous task for host {host} (PID {ptask.process.pid})"
194
183
  )
195
- logging.info(
196
- f"Running `{reduced} [...]/{basename(args[0])} {' '.join(args[1:])}`"
184
+ await ptask.process.kill()
185
+ await ptask.task.cancel()
186
+ self.__tasks.pop(host)
187
+ path = pathlib.Path.expanduser(req.path).resolve()
188
+ args = [str(path), *req.args]
189
+ env = req.env if not self.__args.use_base_env else {**environ, **req.env}
190
+
191
+ info_log = f"Running `[...]/{basename(args[0])} {' '.join(args[1:])}`..."
192
+ if env != {} and env is not None:
193
+ info_log += f"\n... with env {pprint(env)}"
194
+ if self.__args.user is not None:
195
+ info_log += f"... as user {self.__args.user}"
196
+
197
+ logging.info(info_log)
198
+
199
+ try:
200
+ p = await asyncio.create_subprocess_exec(
201
+ *args,
202
+ env=env,
203
+ stdout=asyncio.subprocess.PIPE,
204
+ stderr=asyncio.subprocess.STDOUT,
205
+ user=self.__args.user,
197
206
  )
198
- try:
199
- p = await asyncio.create_subprocess_exec(
200
- *args,
201
- env=env,
202
- stdout=asyncio.subprocess.PIPE,
203
- stderr=asyncio.subprocess.STDOUT,
204
- )
205
- self.__sessions[host] = p
206
- self.__tasks[host] = asyncio.create_task(self.__log(ws, p))
207
- resp = OkayResp()
208
- await ws.send(serialize(resp))
209
- except Exception as e:
210
- logging.error(f"Failed to run process with error {e}")
211
- resp = ErrorResp(msg=f"Caught exception: {e}")
212
- await ws.send(serialize(resp))
207
+ self.__tasks[host] = asyncio.create_task(self.__log(ws, p))
208
+ resp = OkayResp()
209
+ await ws.send(serialize(resp))
210
+ except Exception as e:
211
+ logging.error(f"Failed to run process with error {e}")
212
+ resp = ErrorResp(msg=f"Server error: {e}")
213
+ await ws.send(serialize(resp))
213
214
 
214
215
  async def __log(self, ws: ServerConnection, process: asyncio.subprocess.Process):
215
216
  try:
@@ -233,12 +234,13 @@ class PsyncServer:
233
234
 
234
235
  async def __kill(self, _req: KillReq, ws: ServerConnection):
235
236
  host = self.__get_host(ws)
236
- p = self.__sessions.get(host)
237
- task = self.__tasks.get(host)
238
- if p is not None and task is not None:
239
- logging.info(f"Killing PID {p.pid}")
240
- p.kill()
241
- code = await p.wait()
237
+ ptask = self.__tasks.get(host)
238
+ process = ptask.process
239
+ task = ptask.task
240
+ if process is not None and task is not None:
241
+ logging.info(f"Killing PID {process.pid}")
242
+ process.kill()
243
+ code = await process.wait()
242
244
  resp = ExitResp(str(code))
243
245
  await ws.send(serialize(resp))
244
246
  else:
@@ -255,9 +257,8 @@ class PsyncServer:
255
257
 
256
258
  def main():
257
259
  """Run the server as an executable."""
258
- log_level = environ.get("PSYNC_LOG", "INFO").upper()
259
- logging.basicConfig(handlers=[InterceptHandler()], level=log_level, force=True)
260
- asyncio.run(PsyncServer().serve())
260
+ logging.basicConfig(handlers=[InterceptHandler()], level=PSYNC_LOG, force=True)
261
+ asyncio.run(PsyncServer(parse_args()).serve())
261
262
 
262
263
 
263
264
  if __name__ == "__main__":