-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathrpc_server.py
executable file
·150 lines (127 loc) · 5.03 KB
/
rpc_server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/env python3
# NeosVR-Headless-API
# Copyright (C) 2022 GlitchFur
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# This is the RPC server which can be used to control headless clients remotely.
import logging
import argparse
from rpyc import Service, restricted
from neosvr_headless_api import HeadlessProcess
logging.basicConfig(
format="[%(asctime)s][%(levelname)s] %(message)s", level=logging.INFO
)
def main():
parser = argparse.ArgumentParser(
description="RPC server for controlling multiple headless clients"
)
parser.add_argument(
"--host",
default="127.0.0.1",
help="The host or IP address to bind to. (Default: 127.0.0.1)",
)
parser.add_argument(
"-p",
"--port",
type=int,
default=16881,
help="The TCP port to bind to. (Default: 16881)",
)
args = parser.parse_args()
from rpyc.utils.server import ThreadedServer
server = ThreadedServer(
HeadlessProcessService(), hostname=args.host, port=args.port
)
server.start()
class HeadlessProcessService(Service):
def __init__(self):
super().__init__()
self.processes = {}
self.current_id = 0
def on_connect(self, conn):
host, port = conn._config["endpoints"][1]
logging.info("Client connected from %s:%d" % (host, port))
def on_disconnect(self, conn):
host, port = conn._config["endpoints"][1]
logging.info("Client disconnected from %s:%d" % (host, port))
def exposed_start_headless_process(self, *args, **kwargs):
"""
Start a new instance of a `HeadlessProcess`. Returns a tuple in the form
of (id, `HeadlessProcess` instance). All arguments and keyword arguments
are passed to `HeadlessProcess` as-is.
The id can be used to get the `HeadlessProcess` instance again at a
later time with `get_headless_process()`.
"""
allowed_access = (
"neos_dir",
"config",
"write",
"readline",
"shutdown",
"sigint",
"terminate",
"kill",
"wait",
)
process = restricted(HeadlessProcess(*args, **kwargs), allowed_access)
self.current_id += 1
self.processes[self.current_id] = process
logging.info("Starting headless process with ID: %d" % self.current_id)
logging.info("Neos Dir: %s" % process.neos_dir)
logging.info("Config: %s" % process.config)
logging.info("Total processes running: %d" % len(self.processes))
return (self.current_id, process)
def exposed_stop_headless_process(self, pid):
"""
Stops the `HeadlessProcess` with the given `pid` and removes it from the
process list. Returns the exit code of the process.
"""
process = self.processes[pid]
exit_code = process.shutdown()
del self.processes[pid]
logging.info(
"Headless process with ID %d terminated with return code %d."
% (pid, exit_code)
)
logging.info("Total processes running: %d" % len(self.processes))
return exit_code
def exposed_send_signal_headless_process(self, pid, sig):
"""
Send a signal to the `HeadlessProcess` with the given `pid` and removes
it from the process list. `sig` is an integer which can be either
SIGINT (2), SIGTERM (15), or SIGKILL (9). If `sig` is not one of these
integers, `ValueError` will be raised. Returns the signal used to
terminate the process as a negative integer.
"""
process = self.processes[pid]
if not sig in (2, 9, 15):
raise ValueError("Signal not allowed: %d" % sig)
if sig == 2:
func = process.sigint
elif sig == 9:
func = process.kill
elif sig == 15:
func = process.terminate
exit_code = func()
# TODO: The following code is identical to that of
# `exposed_stop_headless_process()`
del self.processes[pid]
logging.info(
"Headless process with ID %d terminated with return code %d."
% (pid, exit_code)
)
logging.info("Total processes running: %d" % len(self.processes))
return exit_code
def exposed_get_headless_process(self, pid):
"""Returns an existing `HeadlessProcess` with the given `pid`."""
return self.processes[pid]
if __name__ == "__main__":
main()