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.
- {cubething_psync-0.2.1/src/cubething_psync.egg-info → cubething_psync-0.2.1.dev2}/PKG-INFO +1 -1
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/pyproject.toml +1 -1
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/client/main.py +1 -1
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2/src/cubething_psync.egg-info}/PKG-INFO +1 -1
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/SOURCES.txt +1 -0
- cubething_psync-0.2.1.dev2/src/server/args.py +69 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/server/main.py +72 -71
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/LICENSE +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/README.md +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/setup.cfg +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/client/__init__.py +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/client/__main__.py +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/client/args.py +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/common/__init__.py +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/common/data.py +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/common/log.py +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/dependency_links.txt +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/entry_points.txt +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/requires.txt +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/top_level.txt +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/server/__init__.py +0 -0
- {cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/server/__main__.py +0 -0
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
99
|
-
pathlib.Path(
|
|
95
|
+
pathlib.Path(self.__args.cert_path).expanduser(),
|
|
96
|
+
pathlib.Path(self.__args.key_path).expanduser(),
|
|
100
97
|
)
|
|
101
|
-
|
|
98
|
+
logging.debug(pprint(ssl_ctx.get_ca_certs()))
|
|
102
99
|
server = await serve(
|
|
103
100
|
(self.__handle()),
|
|
104
|
-
self.
|
|
105
|
-
self.
|
|
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.
|
|
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.
|
|
185
|
-
self.
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/requires.txt
RENAMED
|
File without changes
|
{cubething_psync-0.2.1 → cubething_psync-0.2.1.dev2}/src/cubething_psync.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|