Skip to content

Commit 6e6b9c0

Browse files
Enhanced discos-simulator script
1 parent 93b9ba5 commit 6e6b9c0

3 files changed

Lines changed: 188 additions & 58 deletions

File tree

scripts/discos-simulator

Lines changed: 133 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,50 @@
66
$ discos-simulator start -s if_distributor
77
$ discos-simulator stop -s if_distributor
88
"""
9-
from __future__ import print_function
109
import importlib
1110
import subprocess
1211
import sys
13-
import time
14-
import socket
12+
import re
13+
import os
1514
from argparse import ArgumentParser, ArgumentTypeError
15+
from concurrent.futures import ThreadPoolExecutor
1616

1717
from simulators.server import Simulator
1818
from simulators.utils import list_simulators
1919

2020
AVAILABLE_SIMULATORS = list_simulators()
21+
PATTERN = re.compile(
22+
r"discos-simulator\s+(?:-s\s+(\S+)\s+start|start\s+-s\s+(\S+))"
23+
)
24+
25+
26+
def running_simulators():
27+
current_pid = os.getpid()
28+
out = subprocess.check_output(["ps", "aux"], text=True)
29+
result = set()
30+
for line in out.splitlines():
31+
parts = line.split(None, 2)
32+
if len(parts) < 3:
33+
continue
34+
try:
35+
pid = int(parts[1])
36+
except ValueError:
37+
continue
38+
if pid == current_pid:
39+
continue
40+
m = PATTERN.search(line)
41+
if m:
42+
n = m.group(1) or m.group(2)
43+
result.add(n)
44+
return sorted(result)
45+
46+
47+
def wait_until_started(process):
48+
for line in process.stdout:
49+
sys.stdout.write(line)
50+
sys.stdout.flush()
51+
if "running" in line:
52+
return
2153

2254

2355
def system_from_arg(system_name):
@@ -35,7 +67,7 @@ def system_from_arg(system_name):
3567
parser = ArgumentParser()
3668
parser.add_argument(
3769
"action",
38-
choices=["start", "stop", "list"]
70+
choices=["start", "stop", "status", "list"]
3971
)
4072
parser.add_argument(
4173
"-s", "--system",
@@ -49,62 +81,106 @@ parser.add_argument(
4981
required=False,
5082
help="System configuration type: IFD_14_channels for if_distributor, ...",
5183
)
52-
args = parser.parse_args()
5384

54-
if args.action == "list":
55-
print(
56-
"Available simulators: '"
57-
+ "', '".join(AVAILABLE_SIMULATORS)
58-
+ "'."
59-
)
85+
if __name__ == "__main__":
86+
kwargs = {}
6087

61-
kwargs = {}
62-
63-
if args.type:
64-
name = args.system.__name__.rsplit(".", 1)[1]
65-
if not args.system:
66-
parser.error(
67-
"The '--type' argument only has to be used "
68-
+ "in conjunction with the '--system' argument."
69-
)
70-
try:
71-
systems = getattr(args.system, "systems")
72-
except AttributeError:
73-
parser.error(
74-
f"System '{name}' has no configurations other than the default one"
75-
+ ". Omit the '--type' argument to start the simulator properly."
76-
)
77-
if args.type not in systems:
78-
err_string = (
79-
f"Configuration '{args.type}' for system '{name}' not found.\n"
80-
)
81-
err_string += "Available configurations for desired simulator:\n"
82-
err_string += "'" + "', ".join(systems) + "'."
83-
parser.error(err_string)
84-
kwargs["system_type"] = args.type
88+
args = parser.parse_args()
8589

86-
if args.action == "start":
87-
if args.system:
90+
if args.type:
91+
sim = args.system.__name__.rsplit(".", 1)[1]
92+
if not args.system:
93+
parser.error(
94+
"The '--type' argument only has to be used "
95+
+ "in conjunction with the '--system' argument."
96+
)
8897
try:
89-
simulator = Simulator(args.system, **kwargs)
90-
simulator.start()
91-
except socket.error:
92-
name = args.system.__name__.rsplit(".", 1)[1]
98+
systems = getattr(args.system, "systems")
99+
except AttributeError:
100+
parser.error(
101+
f"System '{sim}' has no configurations other than the default "
102+
+ "one. Omit the '--type' argument to start the simulator "
103+
+ "properly."
104+
)
105+
if args.type not in systems:
106+
err_string = (
107+
f"Configuration '{args.type}' for system '{sim}' not found.\n"
108+
)
109+
err_string += "Available configurations for desired simulator:\n"
110+
err_string += "'" + "', ".join(systems) + "'."
111+
parser.error(err_string)
112+
kwargs["system_type"] = args.type
113+
114+
if args.action == "list":
115+
print(
116+
"Available simulators: '"
117+
+ "', '".join(AVAILABLE_SIMULATORS)
118+
+ "'."
119+
)
120+
running = running_simulators()
121+
if running:
93122
print(
94-
f"Cannot start simulator '{name}', address already in use."
123+
"Running simulators: '"
124+
+ "', '".join(running)
125+
+ "'."
95126
)
96-
sys.exit(1)
97-
else:
98-
for sim in AVAILABLE_SIMULATORS:
99-
# pylint: disable=consider-using-with
100-
subprocess.Popen([sys.argv[0], "start", "-s", sim])
101-
time.sleep(3)
102-
elif args.action == "stop":
103-
if args.system:
104-
simulator = Simulator(args.system, **kwargs)
105-
simulator.stop()
106-
else:
107-
for sim in AVAILABLE_SIMULATORS:
108-
sim = importlib.import_module(f"simulators.{sim}")
109-
simulator = Simulator(sim, **kwargs)
110-
simulator.stop()
127+
if args.action == "status":
128+
running = running_simulators()
129+
if running:
130+
print(
131+
"Running simulators: '"
132+
+ "', '".join(running)
133+
+ "'."
134+
)
135+
else:
136+
print("No simulator is running.")
137+
elif args.action == "start":
138+
running = running_simulators()
139+
if args.system:
140+
sim = args.system.__name__.split('.')[-1]
141+
if sim not in running:
142+
simulator = Simulator(args.system, **kwargs)
143+
simulator.start()
144+
else:
145+
print(f"Simulator '{sim}' already running.")
146+
else:
147+
processes = []
148+
for sim in AVAILABLE_SIMULATORS:
149+
if sim in running:
150+
print(f"Simulator '{sim}' already running.")
151+
continue
152+
command = \
153+
[sys.executable, "-u", sys.argv[0], "-s", sim, "start"]
154+
# pylint: disable=consider-using-with
155+
p = subprocess.Popen(
156+
command,
157+
stdout=subprocess.PIPE,
158+
stderr=subprocess.PIPE,
159+
text=True,
160+
bufsize=1
161+
)
162+
processes.append(p)
163+
if processes:
164+
futures = []
165+
executor = ThreadPoolExecutor(max_workers=len(processes))
166+
for proc in processes:
167+
fut = executor.submit(wait_until_started, proc)
168+
futures.append(fut)
169+
executor.shutdown(wait=True)
170+
elif args.action == "stop":
171+
running = running_simulators()
172+
if args.system:
173+
name = args.system.__name__.split('.')[-1]
174+
if name not in running:
175+
print(f"Simulator '{name}' is not running.")
176+
else:
177+
simulator = Simulator(args.system, **kwargs)
178+
simulator.stop()
179+
else:
180+
for name in AVAILABLE_SIMULATORS:
181+
if name not in running:
182+
print(f"Simulator '{name}' is not running.")
183+
else:
184+
sim = importlib.import_module(f"simulators.{name}")
185+
simulator = Simulator(sim, **kwargs)
186+
simulator.stop()

simulators/server.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,15 @@ def __init__(
243243
)
244244
if not l_address and not s_address:
245245
raise ValueError('You must specify at least one server.')
246+
for address in (l_address, s_address):
247+
if not address:
248+
continue
249+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
250+
try:
251+
s.bind(address)
252+
finally:
253+
s.close()
254+
246255
self.system_cls = system_cls
247256
self.system_kwargs = kwargs
248257
self.system = None
@@ -342,20 +351,23 @@ def start(
342351
started_servers = []
343352
process_class = mp.get_context(context).Process
344353
executor = threading.Thread
354+
servers = []
345355
try:
346356
for l_addr, s_addr, s_type, kwargs in self.servers:
347357
kwargs.update(self.kwargs)
348358
s = Server(
349359
self.system, s_type, kwargs, l_addr, s_addr
350360
)
361+
servers.append(s)
362+
for s in servers:
351363
started = mp.Event()
352-
started_servers.append(started)
353364
p = executor(
354365
target=s.serve_forever,
355366
args=(started,),
356367
daemon=daemon
357368
)
358369
self.processes.append(p)
370+
started_servers.append(started)
359371
executor = process_class
360372
for process in self.processes:
361373
process.start()
@@ -371,6 +383,8 @@ def start(
371383
except KeyboardInterrupt:
372384
print('') # Skip the line displaying the SIGINT character
373385
self.stop()
386+
except OSError:
387+
print(f"Simulator '{self.simulator_name}' already running.")
374388

375389
def stop(self):
376390
"""Stops a simulator by sending the custom `$system_stop%%%%%` command

tests/test_server.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,24 @@ def test_server_wrong_socket_type(self):
422422
l_address=address
423423
)
424424

425+
def test_server_start_twice(self):
426+
address = next(address_generator)
427+
with self.assertRaises(OSError):
428+
s1 = Server(
429+
ListeningTestSystem,
430+
ThreadingTCPServer,
431+
kwargs={},
432+
l_address=address
433+
)
434+
s1.start()
435+
s2 = Server(
436+
ListeningTestSystem,
437+
ThreadingTCPServer,
438+
kwargs={},
439+
l_address=address
440+
)
441+
s2.start()
442+
425443

426444
class TestSimulator(unittest.TestCase):
427445

@@ -564,6 +582,28 @@ def shutdown(self, event):
564582
simulator.start(has_started=e)
565583
t.join()
566584

585+
def test_start_simulator_twice(self):
586+
try:
587+
address = next(address_generator)
588+
self.mymodule.servers = [(address, (), ThreadingTCPServer, {})]
589+
self.mymodule.System = ListeningTestSystem
590+
591+
s1 = Simulator(self.mymodule)
592+
s1.start(daemon=True)
593+
594+
stdout = StringIO()
595+
sys.stdout = stdout
596+
s2 = Simulator(self.mymodule)
597+
s2.start(daemon=True)
598+
599+
s1.stop()
600+
output = stdout.getvalue()
601+
finally:
602+
sys.stdout = sys.__stdout__
603+
stdout.close()
604+
605+
self.assertIn("already running", output)
606+
567607

568608
def get_response(
569609
server_address,

0 commit comments

Comments
 (0)