edq.testing.serverrunner

  1import argparse
  2import atexit
  3import logging
  4import signal
  5import subprocess
  6import time
  7import typing
  8
  9import edq.net.request
 10import edq.util.dirent
 11
 12DEFAULT_SERVER_STARTUP_INITIAL_WAIT_SECS: float = 0.2
 13DEFAULT_STARTUP_WAIT_SECS: float = 10.0
 14SERVER_STOP_WAIT_SECS: float = 5.00
 15
 16DEFAULT_IDENTIFY_MAX_ATTEMPTS: int = 100
 17DEFAULT_IDENTIFY_WAIT_SECS: float = 0.25
 18
 19_logger = logging.getLogger(__name__)
 20
 21class ServerRunner():
 22    """
 23    A class for running an external HTTP server for some sort of larger process (like testing or generating data).
 24    """
 25
 26    def __init__(self,
 27            server: typing.Union[str, None] = None,
 28            server_start_command: typing.Union[str, None] = None,
 29            server_stop_command: typing.Union[str, None] = None,
 30            http_exchanges_out_dir: typing.Union[str, None] = None,
 31            server_output_path: typing.Union[str, None] = None,
 32            startup_initial_wait_secs: float = DEFAULT_SERVER_STARTUP_INITIAL_WAIT_SECS,
 33            startup_wait_secs: typing.Union[float, None] = None,
 34            startup_skip_identify: typing.Union[bool, None] = False,
 35            identify_max_attempts: int = DEFAULT_IDENTIFY_MAX_ATTEMPTS,
 36            identify_wait_secs: float = DEFAULT_IDENTIFY_WAIT_SECS,
 37            **kwargs: typing.Any) -> None:
 38        if (server is None):
 39            raise ValueError('No server specified.')
 40
 41        self.server: str = server
 42        """ The server address to point requests to. """
 43
 44        if (server_start_command is None):
 45            raise ValueError('No command to start the server was specified.')
 46
 47        self.server_start_command: str = server_start_command
 48        """ The server_start_command to run the LMS server. """
 49
 50        self.server_stop_command: typing.Union[str, None] = server_stop_command
 51        """ An optional command to stop the server. """
 52
 53        if (http_exchanges_out_dir is None):
 54            http_exchanges_out_dir = edq.util.dirent.get_temp_dir(prefix = 'edq-serverrunner-http-exchanges-', rm = False)
 55
 56        self.http_exchanges_out_dir: str = http_exchanges_out_dir
 57        """ Where to output the HTTP exchanges. """
 58
 59        if (server_output_path is None):
 60            server_output_path = edq.util.dirent.get_temp_path(prefix = 'edq-serverrunner-server-output-', rm = False) + '.txt'
 61
 62        self.server_output_path: str = server_output_path
 63        """ Where to write server output (stdout and stderr). """
 64
 65        self.startup_initial_wait_secs: float = startup_initial_wait_secs
 66        """ The duration to wait after giving the initial startup command. """
 67
 68        if (startup_wait_secs is None):
 69            startup_wait_secs = DEFAULT_STARTUP_WAIT_SECS
 70
 71        self.startup_wait_secs = startup_wait_secs
 72        """ How long to wait after the server start command is run before making requests to the server. """
 73
 74        if (startup_skip_identify is None):
 75            startup_skip_identify = False
 76
 77        self.startup_skip_identify: bool = startup_skip_identify
 78        """
 79        Whether to skip trying to identify the server after it has been started.
 80        This acts as a way to have a variable wait for the server to start.
 81        When not used, self.startup_wait_secs is the only way to wait for the server to start.
 82        """
 83
 84        self.identify_max_attempts: int = identify_max_attempts
 85        """ The maximum number of times to try an identity check before starting the server. """
 86
 87        self.identify_wait_secs: float = identify_wait_secs
 88        """ The number of seconds each identify request will wait for the server to respond. """
 89
 90        self._old_exchanges_out_dir: typing.Union[str, None] = None
 91        """
 92        The value of edq.net.request._exchanges_out_dir when start() is called.
 93        The original value may be changed in start(), and will be reset in stop().
 94        """
 95
 96        self._process: typing.Union[subprocess.Popen, None] = None
 97        """ The server process. """
 98
 99        self._server_output_file: typing.Union[typing.IO, None] = None
100        """ The file that server output is written to. """
101
102    def start(self) -> None:
103        """ Start the server. """
104
105        if (self._process is not None):
106            return
107
108        # Ensure stop() is called.
109        atexit.register(self.stop)
110
111        # Store and set networking config.
112
113        self._old_exchanges_out_dir = edq.net.request._exchanges_out_dir
114        edq.net.request._exchanges_out_dir = self.http_exchanges_out_dir
115
116        # Start the server.
117
118        _logger.info("Writing HTTP exchanges to '%s'.", self.http_exchanges_out_dir)
119        _logger.info("Writing server output to '%s'.", self.server_output_path)
120        _logger.info("Starting the server ('%s') and waiting for it.", self.server)
121
122        self._server_output_file = open(self.server_output_path, 'a', encoding = edq.util.dirent.DEFAULT_ENCODING)  # pylint: disable=consider-using-with
123
124        self._start_server()
125        _logger.info("Server is started up.")
126
127    def _start_server(self) -> None:
128        """ Start the server. """
129
130        if (self._process is not None):
131            return
132
133        self._process = subprocess.Popen(self.server_start_command,  # pylint: disable=consider-using-with
134                shell = True, stdout = self._server_output_file, stderr = subprocess.STDOUT)
135
136        status = None
137        try:
138            # Wait for a short period for the process to start.
139            status = self._process.wait(self.startup_initial_wait_secs)
140        except subprocess.TimeoutExpired:
141            # Good, the server is running.
142            pass
143
144        if (status is not None):
145            hint = f"code: '{status}'"
146            if (status == 125):
147                hint = 'server may already be running'
148
149            raise ValueError(f"Server was unable to start successfully ('{hint}').")
150
151        _logger.info("Completed initial server start wait.")
152
153        # Ping the server to check if it has started.
154        if (not self.startup_skip_identify):
155            for _ in range(self.identify_max_attempts):
156                if (self.identify_server()):
157                    # The server is running and responding, exit early.
158                    return
159
160                time.sleep(self.identify_wait_secs)
161
162        status = None
163        try:
164            # Ensure the server is running cleanly.
165            status = self._process.wait(self.startup_wait_secs)
166        except subprocess.TimeoutExpired:
167            # Good, the server is running.
168            pass
169
170        if (status is not None):
171            raise ValueError(f"Server was unable to start successfully ('code: {status}').")
172
173    def stop(self) -> bool:
174        """
175        Stop the server.
176        Return true if child classes should perform shutdown behavior.
177        """
178
179        if (self._process is None):
180            return False
181
182        # Stop the server.
183        _logger.info('Stopping the server.')
184        self._stop_server()
185
186        # Restore networking config.
187
188        edq.net.request._exchanges_out_dir = self._old_exchanges_out_dir
189        self._old_exchanges_out_dir = None
190
191        if (self._server_output_file is not None):
192            self._server_output_file.close()
193            self._server_output_file = None
194
195        return True
196
197    def restart(self) -> None:
198        """ Restart the server. """
199
200        _logger.debug('Restarting the server.')
201        self._stop_server()
202        self._start_server()
203
204    def identify_server(self) ->  bool:
205        """
206        Attempt to identify the target server and return true on a successful attempt.
207        This is used on startup to wait for the server to complete startup.
208
209        Child classes must implement this or set self.startup_skip_identify to true.
210        """
211
212        raise NotImplementedError('identify_server')
213
214    def _stop_server(self) -> typing.Union[int, None]:
215        """ Stop the server process and return the exit status. """
216
217        if (self._process is None):
218            return None
219
220        # Mark the process as dead, so it can be restarted (if need be).
221        current_process = self._process
222        self._process = None
223
224        # Check if the process is already dead.
225        status = current_process.poll()
226        if (status is not None):
227            return status
228
229        # If the user provided a special command, try it.
230        if (self.server_stop_command is not None):
231            subprocess.run(self.server_stop_command,
232                    shell = True, stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL,
233                    check = False)
234
235        status = current_process.poll()
236        if (status is not None):
237            return status
238
239        # Try to end the server gracefully.
240        try:
241            current_process.send_signal(signal.SIGINT)
242            current_process.wait(SERVER_STOP_WAIT_SECS)
243        except subprocess.TimeoutExpired:
244            pass
245
246        status = current_process.poll()
247        if (status is not None):
248            return status
249
250        # End the server hard.
251        try:
252            current_process.kill()
253            current_process.wait(SERVER_STOP_WAIT_SECS)
254        except subprocess.TimeoutExpired:
255            pass
256
257        status = current_process.poll()
258        if (status is not None):
259            return status
260
261        return None
262
263def modify_parser(parser: argparse.ArgumentParser) -> None:
264    """ Modify the parser to add arguments for running a server. """
265
266    parser.add_argument('server_start_command', metavar = 'RUN_SERVER_COMMAND',
267        action = 'store', type = str,
268        help = 'The command to run the LMS server that will be the target of the data generation commands.')
269
270    group = parser.add_argument_group('server control')
271
272    group.add_argument('--server-output-file', dest = 'server_output_path',
273        action = 'store', type = str, default = None,
274        help = 'Where server output will be written. Defaults to a random temp file.')
275
276    group.add_argument('--server-stop-command', dest = 'server_stop_command',
277        action = 'store', type = str, default = None,
278        help = 'An optional command to stop the server. After this the server will be sent a SIGINT and then a SIGKILL.')
279
280    group.add_argument('--startup-skip-identify', dest = 'startup_skip_identify',
281        action = 'store_true', default = False,
282        help = 'If set, startup will skip trying to identify the server as a means of checking that the server is started.')
283
284    group.add_argument('--startup-wait', dest = 'startup_wait_secs',
285        action = 'store', type = float, default = DEFAULT_STARTUP_WAIT_SECS,
286        help = 'The time to wait between starting the server and sending commands (default: %(default)s).')
DEFAULT_SERVER_STARTUP_INITIAL_WAIT_SECS: float = 0.2
DEFAULT_STARTUP_WAIT_SECS: float = 10.0
SERVER_STOP_WAIT_SECS: float = 5.0
DEFAULT_IDENTIFY_MAX_ATTEMPTS: int = 100
DEFAULT_IDENTIFY_WAIT_SECS: float = 0.25
class ServerRunner:
 22class ServerRunner():
 23    """
 24    A class for running an external HTTP server for some sort of larger process (like testing or generating data).
 25    """
 26
 27    def __init__(self,
 28            server: typing.Union[str, None] = None,
 29            server_start_command: typing.Union[str, None] = None,
 30            server_stop_command: typing.Union[str, None] = None,
 31            http_exchanges_out_dir: typing.Union[str, None] = None,
 32            server_output_path: typing.Union[str, None] = None,
 33            startup_initial_wait_secs: float = DEFAULT_SERVER_STARTUP_INITIAL_WAIT_SECS,
 34            startup_wait_secs: typing.Union[float, None] = None,
 35            startup_skip_identify: typing.Union[bool, None] = False,
 36            identify_max_attempts: int = DEFAULT_IDENTIFY_MAX_ATTEMPTS,
 37            identify_wait_secs: float = DEFAULT_IDENTIFY_WAIT_SECS,
 38            **kwargs: typing.Any) -> None:
 39        if (server is None):
 40            raise ValueError('No server specified.')
 41
 42        self.server: str = server
 43        """ The server address to point requests to. """
 44
 45        if (server_start_command is None):
 46            raise ValueError('No command to start the server was specified.')
 47
 48        self.server_start_command: str = server_start_command
 49        """ The server_start_command to run the LMS server. """
 50
 51        self.server_stop_command: typing.Union[str, None] = server_stop_command
 52        """ An optional command to stop the server. """
 53
 54        if (http_exchanges_out_dir is None):
 55            http_exchanges_out_dir = edq.util.dirent.get_temp_dir(prefix = 'edq-serverrunner-http-exchanges-', rm = False)
 56
 57        self.http_exchanges_out_dir: str = http_exchanges_out_dir
 58        """ Where to output the HTTP exchanges. """
 59
 60        if (server_output_path is None):
 61            server_output_path = edq.util.dirent.get_temp_path(prefix = 'edq-serverrunner-server-output-', rm = False) + '.txt'
 62
 63        self.server_output_path: str = server_output_path
 64        """ Where to write server output (stdout and stderr). """
 65
 66        self.startup_initial_wait_secs: float = startup_initial_wait_secs
 67        """ The duration to wait after giving the initial startup command. """
 68
 69        if (startup_wait_secs is None):
 70            startup_wait_secs = DEFAULT_STARTUP_WAIT_SECS
 71
 72        self.startup_wait_secs = startup_wait_secs
 73        """ How long to wait after the server start command is run before making requests to the server. """
 74
 75        if (startup_skip_identify is None):
 76            startup_skip_identify = False
 77
 78        self.startup_skip_identify: bool = startup_skip_identify
 79        """
 80        Whether to skip trying to identify the server after it has been started.
 81        This acts as a way to have a variable wait for the server to start.
 82        When not used, self.startup_wait_secs is the only way to wait for the server to start.
 83        """
 84
 85        self.identify_max_attempts: int = identify_max_attempts
 86        """ The maximum number of times to try an identity check before starting the server. """
 87
 88        self.identify_wait_secs: float = identify_wait_secs
 89        """ The number of seconds each identify request will wait for the server to respond. """
 90
 91        self._old_exchanges_out_dir: typing.Union[str, None] = None
 92        """
 93        The value of edq.net.request._exchanges_out_dir when start() is called.
 94        The original value may be changed in start(), and will be reset in stop().
 95        """
 96
 97        self._process: typing.Union[subprocess.Popen, None] = None
 98        """ The server process. """
 99
100        self._server_output_file: typing.Union[typing.IO, None] = None
101        """ The file that server output is written to. """
102
103    def start(self) -> None:
104        """ Start the server. """
105
106        if (self._process is not None):
107            return
108
109        # Ensure stop() is called.
110        atexit.register(self.stop)
111
112        # Store and set networking config.
113
114        self._old_exchanges_out_dir = edq.net.request._exchanges_out_dir
115        edq.net.request._exchanges_out_dir = self.http_exchanges_out_dir
116
117        # Start the server.
118
119        _logger.info("Writing HTTP exchanges to '%s'.", self.http_exchanges_out_dir)
120        _logger.info("Writing server output to '%s'.", self.server_output_path)
121        _logger.info("Starting the server ('%s') and waiting for it.", self.server)
122
123        self._server_output_file = open(self.server_output_path, 'a', encoding = edq.util.dirent.DEFAULT_ENCODING)  # pylint: disable=consider-using-with
124
125        self._start_server()
126        _logger.info("Server is started up.")
127
128    def _start_server(self) -> None:
129        """ Start the server. """
130
131        if (self._process is not None):
132            return
133
134        self._process = subprocess.Popen(self.server_start_command,  # pylint: disable=consider-using-with
135                shell = True, stdout = self._server_output_file, stderr = subprocess.STDOUT)
136
137        status = None
138        try:
139            # Wait for a short period for the process to start.
140            status = self._process.wait(self.startup_initial_wait_secs)
141        except subprocess.TimeoutExpired:
142            # Good, the server is running.
143            pass
144
145        if (status is not None):
146            hint = f"code: '{status}'"
147            if (status == 125):
148                hint = 'server may already be running'
149
150            raise ValueError(f"Server was unable to start successfully ('{hint}').")
151
152        _logger.info("Completed initial server start wait.")
153
154        # Ping the server to check if it has started.
155        if (not self.startup_skip_identify):
156            for _ in range(self.identify_max_attempts):
157                if (self.identify_server()):
158                    # The server is running and responding, exit early.
159                    return
160
161                time.sleep(self.identify_wait_secs)
162
163        status = None
164        try:
165            # Ensure the server is running cleanly.
166            status = self._process.wait(self.startup_wait_secs)
167        except subprocess.TimeoutExpired:
168            # Good, the server is running.
169            pass
170
171        if (status is not None):
172            raise ValueError(f"Server was unable to start successfully ('code: {status}').")
173
174    def stop(self) -> bool:
175        """
176        Stop the server.
177        Return true if child classes should perform shutdown behavior.
178        """
179
180        if (self._process is None):
181            return False
182
183        # Stop the server.
184        _logger.info('Stopping the server.')
185        self._stop_server()
186
187        # Restore networking config.
188
189        edq.net.request._exchanges_out_dir = self._old_exchanges_out_dir
190        self._old_exchanges_out_dir = None
191
192        if (self._server_output_file is not None):
193            self._server_output_file.close()
194            self._server_output_file = None
195
196        return True
197
198    def restart(self) -> None:
199        """ Restart the server. """
200
201        _logger.debug('Restarting the server.')
202        self._stop_server()
203        self._start_server()
204
205    def identify_server(self) ->  bool:
206        """
207        Attempt to identify the target server and return true on a successful attempt.
208        This is used on startup to wait for the server to complete startup.
209
210        Child classes must implement this or set self.startup_skip_identify to true.
211        """
212
213        raise NotImplementedError('identify_server')
214
215    def _stop_server(self) -> typing.Union[int, None]:
216        """ Stop the server process and return the exit status. """
217
218        if (self._process is None):
219            return None
220
221        # Mark the process as dead, so it can be restarted (if need be).
222        current_process = self._process
223        self._process = None
224
225        # Check if the process is already dead.
226        status = current_process.poll()
227        if (status is not None):
228            return status
229
230        # If the user provided a special command, try it.
231        if (self.server_stop_command is not None):
232            subprocess.run(self.server_stop_command,
233                    shell = True, stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL,
234                    check = False)
235
236        status = current_process.poll()
237        if (status is not None):
238            return status
239
240        # Try to end the server gracefully.
241        try:
242            current_process.send_signal(signal.SIGINT)
243            current_process.wait(SERVER_STOP_WAIT_SECS)
244        except subprocess.TimeoutExpired:
245            pass
246
247        status = current_process.poll()
248        if (status is not None):
249            return status
250
251        # End the server hard.
252        try:
253            current_process.kill()
254            current_process.wait(SERVER_STOP_WAIT_SECS)
255        except subprocess.TimeoutExpired:
256            pass
257
258        status = current_process.poll()
259        if (status is not None):
260            return status
261
262        return None

A class for running an external HTTP server for some sort of larger process (like testing or generating data).

ServerRunner( server: Optional[str] = None, server_start_command: Optional[str] = None, server_stop_command: Optional[str] = None, http_exchanges_out_dir: Optional[str] = None, server_output_path: Optional[str] = None, startup_initial_wait_secs: float = 0.2, startup_wait_secs: Optional[float] = None, startup_skip_identify: Optional[bool] = False, identify_max_attempts: int = 100, identify_wait_secs: float = 0.25, **kwargs: Any)
 27    def __init__(self,
 28            server: typing.Union[str, None] = None,
 29            server_start_command: typing.Union[str, None] = None,
 30            server_stop_command: typing.Union[str, None] = None,
 31            http_exchanges_out_dir: typing.Union[str, None] = None,
 32            server_output_path: typing.Union[str, None] = None,
 33            startup_initial_wait_secs: float = DEFAULT_SERVER_STARTUP_INITIAL_WAIT_SECS,
 34            startup_wait_secs: typing.Union[float, None] = None,
 35            startup_skip_identify: typing.Union[bool, None] = False,
 36            identify_max_attempts: int = DEFAULT_IDENTIFY_MAX_ATTEMPTS,
 37            identify_wait_secs: float = DEFAULT_IDENTIFY_WAIT_SECS,
 38            **kwargs: typing.Any) -> None:
 39        if (server is None):
 40            raise ValueError('No server specified.')
 41
 42        self.server: str = server
 43        """ The server address to point requests to. """
 44
 45        if (server_start_command is None):
 46            raise ValueError('No command to start the server was specified.')
 47
 48        self.server_start_command: str = server_start_command
 49        """ The server_start_command to run the LMS server. """
 50
 51        self.server_stop_command: typing.Union[str, None] = server_stop_command
 52        """ An optional command to stop the server. """
 53
 54        if (http_exchanges_out_dir is None):
 55            http_exchanges_out_dir = edq.util.dirent.get_temp_dir(prefix = 'edq-serverrunner-http-exchanges-', rm = False)
 56
 57        self.http_exchanges_out_dir: str = http_exchanges_out_dir
 58        """ Where to output the HTTP exchanges. """
 59
 60        if (server_output_path is None):
 61            server_output_path = edq.util.dirent.get_temp_path(prefix = 'edq-serverrunner-server-output-', rm = False) + '.txt'
 62
 63        self.server_output_path: str = server_output_path
 64        """ Where to write server output (stdout and stderr). """
 65
 66        self.startup_initial_wait_secs: float = startup_initial_wait_secs
 67        """ The duration to wait after giving the initial startup command. """
 68
 69        if (startup_wait_secs is None):
 70            startup_wait_secs = DEFAULT_STARTUP_WAIT_SECS
 71
 72        self.startup_wait_secs = startup_wait_secs
 73        """ How long to wait after the server start command is run before making requests to the server. """
 74
 75        if (startup_skip_identify is None):
 76            startup_skip_identify = False
 77
 78        self.startup_skip_identify: bool = startup_skip_identify
 79        """
 80        Whether to skip trying to identify the server after it has been started.
 81        This acts as a way to have a variable wait for the server to start.
 82        When not used, self.startup_wait_secs is the only way to wait for the server to start.
 83        """
 84
 85        self.identify_max_attempts: int = identify_max_attempts
 86        """ The maximum number of times to try an identity check before starting the server. """
 87
 88        self.identify_wait_secs: float = identify_wait_secs
 89        """ The number of seconds each identify request will wait for the server to respond. """
 90
 91        self._old_exchanges_out_dir: typing.Union[str, None] = None
 92        """
 93        The value of edq.net.request._exchanges_out_dir when start() is called.
 94        The original value may be changed in start(), and will be reset in stop().
 95        """
 96
 97        self._process: typing.Union[subprocess.Popen, None] = None
 98        """ The server process. """
 99
100        self._server_output_file: typing.Union[typing.IO, None] = None
101        """ The file that server output is written to. """
server: str

The server address to point requests to.

server_start_command: str

The server_start_command to run the LMS server.

server_stop_command: Optional[str]

An optional command to stop the server.

http_exchanges_out_dir: str

Where to output the HTTP exchanges.

server_output_path: str

Where to write server output (stdout and stderr).

startup_initial_wait_secs: float

The duration to wait after giving the initial startup command.

startup_wait_secs

How long to wait after the server start command is run before making requests to the server.

startup_skip_identify: bool

Whether to skip trying to identify the server after it has been started. This acts as a way to have a variable wait for the server to start. When not used, self.startup_wait_secs is the only way to wait for the server to start.

identify_max_attempts: int

The maximum number of times to try an identity check before starting the server.

identify_wait_secs: float

The number of seconds each identify request will wait for the server to respond.

def start(self) -> None:
103    def start(self) -> None:
104        """ Start the server. """
105
106        if (self._process is not None):
107            return
108
109        # Ensure stop() is called.
110        atexit.register(self.stop)
111
112        # Store and set networking config.
113
114        self._old_exchanges_out_dir = edq.net.request._exchanges_out_dir
115        edq.net.request._exchanges_out_dir = self.http_exchanges_out_dir
116
117        # Start the server.
118
119        _logger.info("Writing HTTP exchanges to '%s'.", self.http_exchanges_out_dir)
120        _logger.info("Writing server output to '%s'.", self.server_output_path)
121        _logger.info("Starting the server ('%s') and waiting for it.", self.server)
122
123        self._server_output_file = open(self.server_output_path, 'a', encoding = edq.util.dirent.DEFAULT_ENCODING)  # pylint: disable=consider-using-with
124
125        self._start_server()
126        _logger.info("Server is started up.")

Start the server.

def stop(self) -> bool:
174    def stop(self) -> bool:
175        """
176        Stop the server.
177        Return true if child classes should perform shutdown behavior.
178        """
179
180        if (self._process is None):
181            return False
182
183        # Stop the server.
184        _logger.info('Stopping the server.')
185        self._stop_server()
186
187        # Restore networking config.
188
189        edq.net.request._exchanges_out_dir = self._old_exchanges_out_dir
190        self._old_exchanges_out_dir = None
191
192        if (self._server_output_file is not None):
193            self._server_output_file.close()
194            self._server_output_file = None
195
196        return True

Stop the server. Return true if child classes should perform shutdown behavior.

def restart(self) -> None:
198    def restart(self) -> None:
199        """ Restart the server. """
200
201        _logger.debug('Restarting the server.')
202        self._stop_server()
203        self._start_server()

Restart the server.

def identify_server(self) -> bool:
205    def identify_server(self) ->  bool:
206        """
207        Attempt to identify the target server and return true on a successful attempt.
208        This is used on startup to wait for the server to complete startup.
209
210        Child classes must implement this or set self.startup_skip_identify to true.
211        """
212
213        raise NotImplementedError('identify_server')

Attempt to identify the target server and return true on a successful attempt. This is used on startup to wait for the server to complete startup.

Child classes must implement this or set self.startup_skip_identify to true.

def modify_parser(parser: argparse.ArgumentParser) -> None:
264def modify_parser(parser: argparse.ArgumentParser) -> None:
265    """ Modify the parser to add arguments for running a server. """
266
267    parser.add_argument('server_start_command', metavar = 'RUN_SERVER_COMMAND',
268        action = 'store', type = str,
269        help = 'The command to run the LMS server that will be the target of the data generation commands.')
270
271    group = parser.add_argument_group('server control')
272
273    group.add_argument('--server-output-file', dest = 'server_output_path',
274        action = 'store', type = str, default = None,
275        help = 'Where server output will be written. Defaults to a random temp file.')
276
277    group.add_argument('--server-stop-command', dest = 'server_stop_command',
278        action = 'store', type = str, default = None,
279        help = 'An optional command to stop the server. After this the server will be sent a SIGINT and then a SIGKILL.')
280
281    group.add_argument('--startup-skip-identify', dest = 'startup_skip_identify',
282        action = 'store_true', default = False,
283        help = 'If set, startup will skip trying to identify the server as a means of checking that the server is started.')
284
285    group.add_argument('--startup-wait', dest = 'startup_wait_secs',
286        action = 'store', type = float, default = DEFAULT_STARTUP_WAIT_SECS,
287        help = 'The time to wait between starting the server and sending commands (default: %(default)s).')

Modify the parser to add arguments for running a server.