Skip to content

Commit 1ce985d

Browse files
authored
Merge pull request #326 from ISISComputingGroup/lewis_322
Don't allow control-server thread to try to join itself
2 parents ac72acb + 1e2b0a9 commit 1ce985d

File tree

1 file changed

+53
-35
lines changed

1 file changed

+53
-35
lines changed

lewis/core/simulation.py

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
an :mod:`Adapter <lewis.adapters>`).
2323
"""
2424

25+
from collections.abc import Collection, Sequence
2526
from datetime import datetime
2627
from threading import Thread
2728
from time import sleep
29+
from typing import Any
2830

29-
from lewis.core.adapters import AdapterCollection
31+
from lewis.core.adapters import Adapter, AdapterCollection
3032
from lewis.core.control_server import ControlServer, ExposedObject
31-
from lewis.core.devices import DeviceRegistry
33+
from lewis.core.devices import DeviceBuilder, DeviceRegistry
3234
from lewis.core.logging import has_log
3335
from lewis.core.utils import seconds_since
36+
from lewis.devices import Device
3437

3538

3639
@has_log
@@ -81,7 +84,13 @@ class Simulation:
8184
:param control_server: 'host:port'-string to construct control server or None.
8285
"""
8386

84-
def __init__(self, device, adapters=(), device_builder=None, control_server=None) -> None:
87+
def __init__(
88+
self,
89+
device: Device,
90+
adapters: Sequence[Adapter] = (),
91+
device_builder: DeviceBuilder | None = None,
92+
control_server: str | None = None,
93+
) -> None:
8594
super(Simulation, self).__init__()
8695

8796
self._device_builder = device_builder
@@ -102,7 +111,9 @@ def __init__(self, device, adapters=(), device_builder=None, control_server=None
102111

103112
# Constructing the control server must be deferred until the end,
104113
# because the construction is not complete at this point
105-
self._control_server = None # Just initialize to None and use property setter afterwards
114+
self._control_server: ControlServer | None = (
115+
None # Just initialize to None and use property setter afterwards
116+
)
106117
self._control_server_thread = None
107118
self.control_server = control_server
108119

@@ -115,7 +126,7 @@ def __init__(self, device, adapters=(), device_builder=None, control_server=None
115126
control_server,
116127
)
117128

118-
def _create_control_server(self, control_server):
129+
def _create_control_server(self, control_server: str | None) -> ControlServer | None:
119130
if control_server is None:
120131
return None
121132

@@ -147,14 +158,14 @@ def _create_control_server(self, control_server):
147158
)
148159

149160
@property
150-
def setups(self):
161+
def setups(self) -> list[str]:
151162
"""
152163
A list of setups that are available. Use :meth:`switch_setup` to
153164
change the setup.
154165
"""
155166
return list(self._device_builder.setups.keys()) if self._device_builder is not None else []
156167

157-
def switch_setup(self, new_setup) -> None:
168+
def switch_setup(self, new_setup: str) -> None:
158169
"""
159170
This method switches the setup, which means that it replaces the currently
160171
simulated device with a new device, as defined by the setup.
@@ -164,7 +175,7 @@ def switch_setup(self, new_setup) -> None:
164175
:param new_setup: Name of the new setup to load.
165176
"""
166177
try:
167-
self._device = self._device_builder.create_device(new_setup)
178+
self._device = self._device_builder.create_device(new_setup) # pyright: ignore reportOptionalMemberAccess
168179
self._adapters.set_device(self._device)
169180
self.log.info("Switched setup to '%s'", new_setup)
170181
except Exception as e:
@@ -196,31 +207,34 @@ def start(self) -> None:
196207
while not self._stop_commanded:
197208
delta = self._process_cycle(delta)
198209

210+
self._wait_for_control_server_to_stop()
211+
199212
self._running = False
200213
self._started = False
201214

202215
self.log.info("Simulation has ended.")
203216

204217
def _start_control_server(self) -> None:
205-
if self._control_server is not None and self._control_server_thread is None:
218+
control_server = self._control_server
219+
if control_server is not None and self._control_server_thread is None:
206220

207221
def control_server_loop() -> None:
208-
self._control_server.start_server()
222+
control_server.start_server()
209223

210224
while not self._stop_commanded:
211-
self._control_server.process(blocking=True)
225+
control_server.process(blocking=True)
212226

213227
self.log.info("Stopped processing control server commands, ending thread.")
214228

215229
self._control_server_thread = Thread(target=control_server_loop)
216230
self._control_server_thread.start()
217231

218-
def _stop_control_server(self) -> None:
232+
def _wait_for_control_server_to_stop(self) -> None:
219233
if self._control_server_thread is not None:
220234
self._control_server_thread.join(timeout=1.0)
221235
self._control_server_thread = None
222236

223-
def _process_cycle(self, delta):
237+
def _process_cycle(self, delta: float) -> float:
224238
"""
225239
Processes one cycle, which consists of one simulation cycle and processing
226240
of control server commands. The method measures how long all this takes
@@ -237,7 +251,7 @@ def _process_cycle(self, delta):
237251

238252
return delta
239253

240-
def _process_simulation_cycle(self, delta) -> None:
254+
def _process_simulation_cycle(self, delta: float) -> None:
241255
"""
242256
If the simulation is not paused, the device's process-method is
243257
called with the supplied delta, multiplied by the simulation speed.
@@ -261,15 +275,15 @@ def _process_simulation_cycle(self, delta) -> None:
261275
self._runtime += delta_simulation
262276

263277
@property
264-
def cycle_delay(self):
278+
def cycle_delay(self) -> float:
265279
"""
266280
Desired time between simulation cycles, this can not be negative.
267281
Use 0 for highest possible processing rate.
268282
"""
269283
return self._cycle_delay
270284

271285
@cycle_delay.setter
272-
def cycle_delay(self, delay) -> None:
286+
def cycle_delay(self, delay: float) -> None:
273287
if delay < 0.0:
274288
raise ValueError("Cycle delay can not be negative.")
275289

@@ -278,14 +292,14 @@ def cycle_delay(self, delay) -> None:
278292
self.log.info("Changed cycle delay to %s", self._cycle_delay)
279293

280294
@property
281-
def cycles(self):
295+
def cycles(self) -> int:
282296
"""
283297
Simulation cycles processed since start has been called.
284298
"""
285299
return self._cycles
286300

287301
@property
288-
def uptime(self):
302+
def uptime(self) -> float:
289303
"""
290304
Elapsed time in seconds since the simulation has been started.
291305
"""
@@ -294,7 +308,7 @@ def uptime(self):
294308
return seconds_since(self._start_time)
295309

296310
@property
297-
def speed(self):
311+
def speed(self) -> float:
298312
"""
299313
Simulation speed. Actual elapsed time is multiplied with this property
300314
to determine simulated time. Values greater than 1 increase the simulation
@@ -304,7 +318,7 @@ def speed(self):
304318
return self._speed
305319

306320
@speed.setter
307-
def speed(self, new_speed) -> None:
321+
def speed(self, new_speed: float) -> None:
308322
if new_speed < 0:
309323
raise ValueError("Speed can not be negative.")
310324

@@ -313,14 +327,14 @@ def speed(self, new_speed) -> None:
313327
self.log.info("Changed speed to %s", self._speed)
314328

315329
@property
316-
def runtime(self):
330+
def runtime(self) -> float:
317331
"""
318332
The accumulated simulation time. Whenever speed is different from 1, this
319333
progresses at a different rate than uptime.
320334
"""
321335
return self._runtime
322336

323-
def set_device_parameters(self, parameters) -> None:
337+
def set_device_parameters(self, parameters: dict[str, Any]) -> None:
324338
"""
325339
Set multiple parameters of the simulated device "simultaneously". The passed
326340
parameter is assumed to be device parameter/value dict.
@@ -377,26 +391,24 @@ def stop(self) -> None:
377391
self.log.warning("Stopping simulation")
378392

379393
self._stop_commanded = True
380-
381-
self._stop_control_server()
382394
self._adapters.disconnect()
383395

384396
@property
385-
def is_started(self):
397+
def is_started(self) -> bool:
386398
"""
387399
This property is true if the simulation has been started.
388400
"""
389401
return self._started
390402

391403
@property
392-
def is_paused(self):
404+
def is_paused(self) -> bool:
393405
"""
394406
True if the simulation is paused (implies that the simulation has been started).
395407
"""
396408
return self._started and not self._running
397409

398410
@property
399-
def control_server(self):
411+
def control_server(self) -> ControlServer | None:
400412
"""
401413
ControlServer-instance that exposes the object to remote machines. Can only
402414
be set before start has been called or on a running simulation if no
@@ -406,7 +418,7 @@ def control_server(self):
406418
return self._control_server
407419

408420
@control_server.setter
409-
def control_server(self, control_server) -> None:
421+
def control_server(self, control_server: str | None) -> None:
410422
if self.is_started and self._control_server:
411423
raise RuntimeError("Can not replace control server while simulation is running.")
412424

@@ -437,19 +449,25 @@ class SimulationFactory:
437449
.. warning:: This class is meant for internal use at the moment and may change frequently.
438450
"""
439451

440-
def __init__(self, devices_package) -> None:
452+
def __init__(self, devices_package: str) -> None:
441453
self._reg = DeviceRegistry(devices_package)
442454

443455
@property
444-
def devices(self):
456+
def devices(self) -> Collection[str]:
445457
"""Names of available devices."""
446458
return self._reg.devices
447459

448-
def get_protocols(self, device):
460+
def get_protocols(self, device: str) -> list[str]:
449461
"""Returns a list of available protocols for the specified device."""
450462
return self._reg.device_builder(device).protocols
451463

452-
def create(self, device, setup=None, protocols=None, control_server=None):
464+
def create(
465+
self,
466+
device: str,
467+
setup: str | None = None,
468+
protocols: dict[str, dict[str, Any]] | None = None,
469+
control_server: str | None = None,
470+
) -> Simulation:
453471
"""
454472
Creates a :class:`Simulation` according to the supplied parameters.
455473
@@ -463,22 +481,22 @@ def create(self, device, setup=None, protocols=None, control_server=None):
463481
"""
464482

465483
device_builder = self._reg.device_builder(device)
466-
device = device_builder.create_device(setup)
484+
device_instance = device_builder.create_device(setup)
467485

468486
adapters = []
469487

470488
if protocols is not None:
471489
for protocol, options in protocols.items():
472490
interface = device_builder.create_interface(protocol)
473-
interface.device = device
491+
interface.device = device_instance
474492

475493
adapter = interface.adapter(options=options or {})
476494
adapter.interface = interface
477495

478496
adapters.append(adapter)
479497

480498
return Simulation(
481-
device=device,
499+
device=device_instance,
482500
adapters=adapters,
483501
device_builder=device_builder,
484502
control_server=control_server,

0 commit comments

Comments
 (0)