fprime-gds 3.4.3__py3-none-any.whl → 3.4.4a1__py3-none-any.whl

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.
@@ -9,6 +9,8 @@ adapter for use with the comm-layer.
9
9
  @author lestarch
10
10
  """
11
11
  import abc
12
+ from typing import Type
13
+ from fprime_gds.plugin.definitions import gds_plugin_implementation, gds_plugin_specification
12
14
 
13
15
 
14
16
  class BaseAdapter(abc.ABC):
@@ -47,71 +49,41 @@ class BaseAdapter(abc.ABC):
47
49
  """
48
50
 
49
51
  @classmethod
50
- @abc.abstractmethod
51
- def get_arguments(cls):
52
- """
53
- Returns a set of arguments consumed by this adapter. This will be consumed by the CLI layer in order to provide
54
- command line arguments to the user. Note: these should be globally unique args, e.g. --ip-address
52
+ @gds_plugin_specification
53
+ def register_communication_plugin(cls) -> Type["BaseAdapter"]:
54
+ """Register a communications adapter
55
55
 
56
- :return: dictionary, keys of tuple of arg flags and value of list of other arguments to argparse's add_argument
57
- """
56
+ Plugin hook for registering a plugin that supplies an adapter to the communications interface (radio, uart, i2c,
57
+ etc). This interface is expected to read and write bytes from a wire and will be provided to the framing system.
58
58
 
59
- @classmethod
60
- @abc.abstractmethod
61
- def check_arguments(cls, args):
62
- """
63
- Code that should check arguments of this adapter. If there is a problem with this code, then a "ValueError"
64
- should be raised describing the problem with these arguments.
59
+ Note: users should return the class, not an instance of the class. Needed arguments for instantiation are
60
+ determined from class methods, solicited via the command line, and provided at construction time to the chosen
61
+ instantiation.
65
62
 
66
- :param args: arguments as dictionary
63
+ Returns:
64
+ BaseAdapter subclass
67
65
  """
66
+ raise NotImplementedError()
67
+
68
+
69
+ class NoneAdapter(BaseAdapter):
70
+ """ None adapter used to turn off the comm script """
68
71
 
69
72
  @classmethod
70
- def get_adapters(cls):
71
- """
72
- Get all known adapters of this base class. These must be imported into the comm-layer to be available to the
73
- system, however; they only need to be imported. Once imported this function will find them and make them
74
- available to the comm-layer in the standard way.
73
+ def get_name(cls):
74
+ """ Get name of the non-adapter """
75
+ return "none"
75
76
 
76
- :return: list of all imported comm adapters.
77
- """
78
- adapter_map = {}
79
- for adapter in cls.__subclasses__():
80
- # Get two versions of names
81
- adapter_name = adapter.__module__
82
- adapter_short = adapter_name.split(".")[-1]
83
- # Check to use long or short name
84
- if adapter_short not in adapter_map:
85
- adapter_name = adapter_short
86
- adapter_map[adapter_name] = adapter
87
- return adapter_map
88
-
89
- @staticmethod
90
- def process_arguments(clazz, args):
91
- """
92
- Process arguments incoming from the command line and construct a dictionary of kwargs to supply to the chosen
93
- adapter at creation time. This will allow the user to choose any imported adapters at runtime.
77
+ def read(self, timeout=0.500):
78
+ """ Raise exception if this is called"""
79
+ raise NotImplementedError()
94
80
 
95
- :param args: arguments to process
96
- :return: dictionary of constructor keyword arguments
97
- """
98
- return {
99
- value["dest"]: getattr(args, value["dest"])
100
- for value in clazz.get_arguments().values()
101
- }
81
+ def write(self, frame):
82
+ """ Raise exception if this is called"""
83
+ raise NotImplementedError()
102
84
 
103
85
  @classmethod
104
- def construct_adapter(cls, adapter_name, args):
105
- """
106
- Constructs a new adapter, from the given adapter name and the given namespace of argument inputs. This is a
107
- wrapper of "get_adapters" and "process_arguments" to help build a new, fully configured, adapter. This is a
108
- factory method.
109
-
110
- :param adapter_name: name of the adapter to build
111
- :param args: namespace of arg value to help build an adapter
112
- :return: newly constructed adapter
113
- """
114
- if adapter_name == "none":
115
- return None
116
- adapter = cls.get_adapters()[adapter_name]
117
- return adapter(**cls.process_arguments(adapter, args))
86
+ @gds_plugin_implementation
87
+ def register_communication_plugin(cls):
88
+ """ Register this as a plugin """
89
+ return cls
@@ -18,6 +18,8 @@ import time
18
18
  import fprime_gds.common.communication.adapters.base
19
19
  import fprime_gds.common.logger
20
20
 
21
+ from fprime_gds.plugin.definitions import gds_plugin_implementation
22
+
21
23
  LOGGER = logging.getLogger("ip_adapter")
22
24
 
23
25
 
@@ -114,7 +116,7 @@ class IpAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
114
116
 
115
117
  def write(self, frame):
116
118
  """
117
- Send a given framed bit of data by sending it out the serial interface. It will attempt to reconnect if there is
119
+ Send a given framed bit of data by sending it out the serial interface. It will attempt to reconnect if there
118
120
  was a problem previously. This function will return true on success, or false on error.
119
121
 
120
122
  :param frame: framed data packet to send out
@@ -151,6 +153,11 @@ class IpAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
151
153
  self.write(IpAdapter.KEEPALIVE_DATA)
152
154
  time.sleep(interval)
153
155
 
156
+ @classmethod
157
+ def get_name(cls):
158
+ """ Get the name of this adapter """
159
+ return "ip"
160
+
154
161
  @classmethod
155
162
  def get_arguments(cls):
156
163
  """
@@ -163,13 +170,13 @@ class IpAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
163
170
  "dest": "address",
164
171
  "type": str,
165
172
  "default": "0.0.0.0",
166
- "help": "Address of the IP adapter server. Default: %(default)s",
173
+ "help": "Address of the IP adapter server.",
167
174
  },
168
175
  ("--ip-port",): {
169
176
  "dest": "port",
170
177
  "type": int,
171
178
  "default": 50000,
172
- "help": "Port of the IP adapter server. Default: %(default)s",
179
+ "help": "Port of the IP adapter server.",
173
180
  },
174
181
  ("--ip-client",): {
175
182
  # dest is "server" since it is handled in BaseAdapter.construct_adapter and passed with the same
@@ -182,14 +189,25 @@ class IpAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
182
189
  }
183
190
 
184
191
  @classmethod
185
- def check_arguments(cls, args):
192
+ @gds_plugin_implementation
193
+ def register_communication_plugin(cls):
194
+ """ Register this as a communication plugin """
195
+ return cls
196
+
197
+ @classmethod
198
+ def check_arguments(cls, address, port, server=True):
186
199
  """
187
200
  Code that should check arguments of this adapter. If there is a problem with this code, then a "ValueError"
188
201
  should be raised describing the problem with these arguments.
189
202
 
190
203
  :param args: arguments as dictionary
191
204
  """
192
- check_port(args["address"], args["port"])
205
+ try:
206
+ if server:
207
+ check_port(address, port)
208
+ except OSError as os_error:
209
+ raise ValueError(f"{os_error}")
210
+
193
211
 
194
212
 
195
213
  class IpHandler(abc.ABC):
@@ -15,6 +15,8 @@ from serial.tools import list_ports
15
15
 
16
16
  import fprime_gds.common.communication.adapters.base
17
17
 
18
+ from fprime_gds.plugin.definitions import gds_plugin_implementation
19
+
18
20
  LOGGER = logging.getLogger("serial_adapter")
19
21
 
20
22
 
@@ -151,18 +153,29 @@ class SerialAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
151
153
  "dest": "device",
152
154
  "type": str,
153
155
  "default": default,
154
- "help": "UART device representing the FSW. Default: %(default)s",
156
+ "help": "UART device representing the FSW.",
155
157
  },
156
158
  ("--uart-baud",): {
157
159
  "dest": "baud",
158
160
  "type": int,
159
161
  "default": 9600,
160
- "help": "Baud rate of the serial device. Default: %(default)s",
162
+ "help": "Baud rate of the serial device.",
161
163
  },
162
164
  }
163
165
 
164
166
  @classmethod
165
- def check_arguments(cls, args):
167
+ def get_name(cls):
168
+ """ Get name of the adapter """
169
+ return "uart"
170
+
171
+ @classmethod
172
+ @gds_plugin_implementation
173
+ def register_communication_plugin(cls):
174
+ """ Register this as a communication plugin """
175
+ return cls
176
+
177
+ @classmethod
178
+ def check_arguments(cls, device, baud):
166
179
  """
167
180
  Code that should check arguments of this adapter. If there is a problem with this code, then a "ValueError"
168
181
  should be raised describing the problem with these arguments.
@@ -170,16 +183,16 @@ class SerialAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
170
183
  :param args: arguments as dictionary
171
184
  """
172
185
  ports = map(lambda info: info.device, list_ports.comports(include_links=True))
173
- if args["device"] not in ports:
174
- msg = f"Serial port '{args['device']}' not valid. Available ports: {ports}"
186
+ if device not in ports:
187
+ msg = f"Serial port '{device}' not valid. Available ports: {ports}"
175
188
  raise ValueError(
176
189
  msg
177
190
  )
178
191
  # Note: baud rate may not *always* work. These are a superset
179
192
  try:
180
- baud = int(args["baud"])
193
+ baud = int(baud)
181
194
  except ValueError:
182
- msg = f"Serial baud rate '{args['baud']}' not integer. Use one of: {SerialAdapter.BAUDS}"
195
+ msg = f"Serial baud rate '{baud}' not integer. Use one of: {SerialAdapter.BAUDS}"
183
196
  raise ValueError(
184
197
  msg
185
198
  )
@@ -11,7 +11,6 @@ def crc_calculation(data: bytes):
11
11
  return zlib.crc32(data) & 0xFFFFFFFF
12
12
 
13
13
 
14
- CHECKSUM_SELECTION = "crc32"
15
14
  CHECKSUM_MAPPING = {
16
15
  "fixed": lambda data: 0xCAFECAFE,
17
16
  "crc32": crc_calculation,
@@ -19,8 +18,7 @@ CHECKSUM_MAPPING = {
19
18
  }
20
19
 
21
20
 
22
- def calculate_checksum(data: bytes):
21
+ def calculate_checksum(data: bytes, selected_checksum: str):
23
22
  """Calculates the checksum of bytes"""
24
- selected_checksum = CHECKSUM_SELECTION
25
23
  hash_fn = CHECKSUM_MAPPING.get(selected_checksum, CHECKSUM_MAPPING.get("default"))
26
24
  return hash_fn(data)
@@ -15,8 +15,10 @@ import abc
15
15
  import copy
16
16
  import struct
17
17
  import sys
18
+ from typing import Type
18
19
 
19
- from .checksum import calculate_checksum
20
+ from .checksum import calculate_checksum, CHECKSUM_MAPPING
21
+ from fprime_gds.plugin.definitions import gds_plugin_implementation, gds_plugin_specification
20
22
 
21
23
 
22
24
  class FramerDeframer(abc.ABC):
@@ -70,6 +72,24 @@ class FramerDeframer(abc.ABC):
70
72
  return packets, data, discarded_aggregate
71
73
  packets.append(packet)
72
74
 
75
+ @classmethod
76
+ @gds_plugin_specification
77
+ def register_framing_plugin(cls) -> Type["FramerDeframer"]:
78
+ """Register a plugin to provide framing capabilities
79
+
80
+ Plugin hook for registering a plugin that supplies a FramerDeframer implementation. Implementors of this hook must
81
+ return a non-abstract subclass of FramerDeframer. This class will be provided as a framing implementation option
82
+ that users may select via command line arguments.
83
+
84
+ Note: users should return the class, not an instance of the class. Needed arguments for instantiation are
85
+ determined from class methods, solicited via the command line, and provided at construction time to the chosen
86
+ instantiation.
87
+
88
+ Returns:
89
+ FramerDeframer subclass
90
+ """
91
+ raise NotImplementedError()
92
+
73
93
 
74
94
  class FpFramerDeframer(FramerDeframer):
75
95
  """
@@ -97,10 +117,11 @@ class FpFramerDeframer(FramerDeframer):
97
117
  HEADER_FORMAT = None
98
118
  START_TOKEN = None
99
119
 
100
- def __init__(self):
120
+ def __init__(self, checksum_type):
101
121
  """Sets constants on construction."""
102
122
  # Setup the constants as soon as possible.
103
123
  FpFramerDeframer.set_constants()
124
+ self.checksum = checksum_type
104
125
 
105
126
  @classmethod
106
127
  def set_constants(cls):
@@ -134,7 +155,7 @@ class FpFramerDeframer(FramerDeframer):
134
155
  FpFramerDeframer.HEADER_FORMAT, FpFramerDeframer.START_TOKEN, len(data)
135
156
  )
136
157
  framed += data
137
- framed += struct.pack(">I", calculate_checksum(framed))
158
+ framed += struct.pack(">I", calculate_checksum(framed, self.checksum))
138
159
  return framed
139
160
 
140
161
  def deframe(self, data, no_copy=False):
@@ -176,7 +197,8 @@ class FpFramerDeframer(FramerDeframer):
176
197
  )
177
198
  # If the checksum is valid, return the packet. Otherwise continue to rotate
178
199
  if check == calculate_checksum(
179
- data[: data_size + FpFramerDeframer.HEADER_SIZE]
200
+ data[: data_size + FpFramerDeframer.HEADER_SIZE],
201
+ self.checksum
180
202
  ):
181
203
  data = data[total_size:]
182
204
  return deframed, data, discarded
@@ -192,6 +214,33 @@ class FpFramerDeframer(FramerDeframer):
192
214
  return None, data, discarded
193
215
  return None, data, discarded
194
216
 
217
+ @classmethod
218
+ def get_name(cls):
219
+ """ Get the name of this plugin """
220
+ return "fprime"
221
+
222
+ @classmethod
223
+ def get_arguments(cls):
224
+ """ Get arguments for the framer/deframer """
225
+ return {("--comm-checksum-type",): {
226
+ "dest": "checksum_type",
227
+ "action": "store",
228
+ "type": str,
229
+ "help": "Setup the checksum algorithm. [default: %(default)s]",
230
+ "choices": [
231
+ item
232
+ for item in CHECKSUM_MAPPING.keys()
233
+ if item != "default"
234
+ ],
235
+ "default": "crc32",
236
+ }}
237
+
238
+ @classmethod
239
+ @gds_plugin_implementation
240
+ def register_framing_plugin(cls):
241
+ """ Register a bad plugin """
242
+ return cls
243
+
195
244
 
196
245
  class TcpServerFramerDeframer(FramerDeframer):
197
246
  """
@@ -9,6 +9,7 @@ replace the ThreadedTcpServer for several reasons as described below.
9
9
 
10
10
  @author lestarch
11
11
  """
12
+
12
13
  import logging
13
14
  import struct
14
15
  from typing import Tuple
@@ -24,6 +25,7 @@ from fprime_gds.common.transport import (
24
25
 
25
26
  LOGGER = logging.getLogger("transport")
26
27
 
28
+
27
29
  class ZmqWrapper(object):
28
30
  """Handler for ZMQ functions for use in other objects"""
29
31
 
@@ -50,7 +52,9 @@ class ZmqWrapper(object):
50
52
  sub_topic: subscription topic used to filter incoming messages
51
53
  pub_topic: publication topic supplied for remote subscription filters
52
54
  """
53
- assert len(transport_url) == 2, f"Must supply a pair of URLs for ZeroMQ not '{transport_url}'"
55
+ assert (
56
+ len(transport_url) == 2
57
+ ), f"Must supply a pair of URLs for ZeroMQ not '{transport_url}'"
54
58
  self.pub_topic = pub_topic
55
59
  self.sub_topic = sub_topic
56
60
  self.transport_url = transport_url
@@ -78,8 +82,12 @@ class ZmqWrapper(object):
78
82
  The connection is made using self.transport_url, and as such, this must be configured before running. This is
79
83
  intended to be called on the sending thread.
80
84
  """
81
- assert self.transport_url is not None and len(self.transport_url) == 2, "Must configure before connecting"
82
- assert self.zmq_socket_outgoing is None, "Cannot connect outgoing multiple times"
85
+ assert (
86
+ self.transport_url is not None and len(self.transport_url) == 2
87
+ ), "Must configure before connecting"
88
+ assert (
89
+ self.zmq_socket_outgoing is None
90
+ ), "Cannot connect outgoing multiple times"
83
91
  assert self.pub_topic is not None, "Must configure sockets before connecting"
84
92
  self.zmq_socket_outgoing = self.context.socket(zmq.PUB)
85
93
  self.zmq_socket_outgoing.setsockopt(zmq.SNDHWM, 0)
@@ -93,7 +101,7 @@ class ZmqWrapper(object):
93
101
  self.zmq_socket_outgoing.connect(self.transport_url[0])
94
102
 
95
103
  def connect_incoming(self):
96
- """ Sets up a ZeroMQ connection for incoming data
104
+ """Sets up a ZeroMQ connection for incoming data
97
105
 
98
106
  ZeroMQ allows multiple connections to a single endpoint. This only affects incoming connections as sockets must
99
107
  be created on their owning threads. This will connect the ZeroMQ topology and if self.server is set, will bind
@@ -102,8 +110,12 @@ class ZmqWrapper(object):
102
110
  The connection is made using self.transport_url, and as such, this must be configured before running. This is
103
111
  intended to be called on the receiving thread.
104
112
  """
105
- assert self.transport_url is not None and len(self.transport_url) == 2, "Must configure before connecting"
106
- assert self.zmq_socket_incoming is None, "Cannot connect incoming multiple times"
113
+ assert (
114
+ self.transport_url is not None and len(self.transport_url) == 2
115
+ ), "Must configure before connecting"
116
+ assert (
117
+ self.zmq_socket_incoming is None
118
+ ), "Cannot connect incoming multiple times"
107
119
  assert self.sub_topic is not None, "Must configure sockets before connecting"
108
120
  self.zmq_socket_incoming = self.context.socket(zmq.SUB)
109
121
  self.zmq_socket_incoming.setsockopt(zmq.RCVHWM, 0)
@@ -125,7 +137,7 @@ class ZmqWrapper(object):
125
137
  self.zmq_socket_incoming.close()
126
138
 
127
139
  def terminate(self):
128
- """ Terminate the ZeroMQ context"""
140
+ """Terminate the ZeroMQ context"""
129
141
  self.context.term()
130
142
 
131
143
  def recv(self, timeout=None):
@@ -162,11 +174,14 @@ class ZmqClient(ThreadedTransportClient):
162
174
  self.zmq = ZmqWrapper()
163
175
 
164
176
  def connect(
165
- self, transport_url: Tuple[str], sub_routing: RoutingTag, pub_routing: RoutingTag
177
+ self,
178
+ transport_url: Tuple[str],
179
+ sub_routing: RoutingTag,
180
+ pub_routing: RoutingTag,
166
181
  ):
167
182
  """Connects to the ZeroMQ network"""
168
183
  self.zmq.configure(transport_url, sub_routing.value, pub_routing.value)
169
- self.zmq.connect_outgoing() # Outgoing socket, for clients, exists on the current thread
184
+ self.zmq.connect_outgoing() # Outgoing socket, for clients, exists on the current thread
170
185
  super().connect(transport_url, sub_routing, pub_routing)
171
186
 
172
187
  def disconnect(self):
@@ -176,16 +191,18 @@ class ZmqClient(ThreadedTransportClient):
176
191
 
177
192
  def send(self, data):
178
193
  """Send data via ZeroMQ"""
179
- if data[:4] == b'ZZZZ':
194
+ if data[:4] == b"ZZZZ":
180
195
  data = data[4:]
181
- self.zmq.send(data) # Must strip out ZZZZ as that is a ThreadedTcpServer only property
196
+ self.zmq.send(
197
+ data
198
+ ) # Must strip out ZZZZ as that is a ThreadedTcpServer only property
182
199
 
183
200
  def recv(self, timeout=None):
184
201
  """Receives data from ZeroMQ"""
185
202
  return self.zmq.recv(timeout)
186
203
 
187
204
  def recv_thread(self):
188
- """ Overrides the recv_thread method
205
+ """Overrides the recv_thread method
189
206
 
190
207
  Overrides the recv_thread method of the superclass such that the ZeroMQ socket may be created/destroyed
191
208
  before/after the main recv loop.
@@ -203,11 +220,11 @@ class ZmqGround(GroundHandler):
203
220
  to the display and processing layer(s). This effectively acts as the "FSW" side of that interface as it
204
221
  frames/deframes packets heading to that layer.
205
222
 
206
- Since there is likely only one communications client to the FSW users should call make_server() after construction
223
+ Since there is likely only one communications client to the FSW users should instantiate with server=True
207
224
  to ensure that it binds to resources for the network. This is not forced in case of multiple FSW connections.
208
225
  """
209
226
 
210
- def __init__(self, transport_url):
227
+ def __init__(self, transport_url, server=True):
211
228
  """Initialize this interface with the transport_url needed to connect
212
229
 
213
230
  Args:
@@ -217,6 +234,8 @@ class ZmqGround(GroundHandler):
217
234
  self.zmq = ZmqWrapper()
218
235
  self.transport_url = transport_url
219
236
  self.timeout = 10
237
+ if server:
238
+ self.zmq.make_server()
220
239
 
221
240
  def open(self):
222
241
  """Open this ground interface. Delegates to the connect method
@@ -242,10 +261,6 @@ class ZmqGround(GroundHandler):
242
261
  self.zmq.disconnect_outgoing()
243
262
  self.zmq.terminate()
244
263
 
245
- def make_server(self):
246
- """Makes it into a server"""
247
- self.zmq.make_server()
248
-
249
264
  def receive_all(self):
250
265
  """Receive all available packets
251
266
 
@@ -0,0 +1,150 @@
1
+ """ fprime_gds.executables.apps: an implementation of start-up apps in fprime
2
+
3
+ There are twp ways to approach start=up applications in fprime. First, is to implement a run method via a subclass of
4
+ `GdsFunction`. This gives the implementor the ability to run anything within the run function that python offers,
5
+ however; this comes with complexity of setting up a new thread/process/isolation to ensure that the plugin does not
6
+ threaten the fprime-gds core functionality and processes.
7
+
8
+ The second method is to inherit from `GdsApp` implementing the `get_process_invocation` function to return the necessary
9
+ command line that will be spun into its own process.
10
+
11
+ @author lestarch
12
+ """
13
+ import subprocess
14
+ from abc import ABC, abstractmethod
15
+ from typing import List, Type
16
+
17
+ from fprime_gds.plugin.definitions import gds_plugin_specification
18
+
19
+
20
+ class GdsBaseFunction(ABC):
21
+ """ Base functionality for pluggable GDS start-up functions
22
+
23
+ GDS start-up functionality is pluggable. This class acts as a base for pluggable functionality supplies helpers to
24
+ the various start-up plugins.
25
+
26
+ Developers who intend to run in an isolated subprocess are strongly encouraged to use `GdsApp` (see below).
27
+ Developers who need flexibility may use GdsFunction.
28
+ """
29
+
30
+ @abstractmethod
31
+ def run(self):
32
+ """ Run the start-up function
33
+
34
+ Run the start-up function unconstrained by the limitations of running in a dedicated subprocess.
35
+
36
+ """
37
+ raise NotImplementedError()
38
+
39
+
40
+ class GdsFunction(GdsBaseFunction, ABC):
41
+ """ Functionality for pluggable GDS start-up functions
42
+
43
+ GDS start-up functionality is pluggable. This class acts as a wide-open implementation of functionality via a single
44
+ `run` callback. Developers have complete control of the start-up functionality. However, this comes at the cost of
45
+ instability in that case of poorly designed functions.
46
+
47
+ Developers who intend to run in an isolated subprocess are strongly encouraged to use `GdsApp` (see below).
48
+
49
+ Plugin developers are required to implement a single function `run`, which must take care of setting up and running
50
+ the start-up function. Developers **must** handle the isolation of this functionality including spinning off a new
51
+ thread, subprocess, etc. Additionally, the developer must define the `register_gds_function_plugin` class method
52
+ annotated with the @gds_plugin_implementation annotation.
53
+
54
+ Standard plug-in functions (get_name, get_arguments) are available should the implementer desire these features.
55
+ Arguments will be supplied to the class's `__init__` function.
56
+ """
57
+
58
+ @classmethod
59
+ @gds_plugin_specification
60
+ def register_gds_function_plugin(cls) -> Type["GdsFunction"]:
61
+ """Register gds start-up functionality
62
+
63
+ Plugin hook for registering a plugin that supplies start-up functionality. This functionality will run on start-up
64
+ of the GDS network.
65
+
66
+ Note: users should return the class, not an instance of the class. Needed arguments for instantiation are
67
+ determined from class methods, solicited via the command line, and provided at construction time to the chosen
68
+ instantiation.
69
+
70
+ Returns:
71
+ GDSFunction subclass
72
+ """
73
+ raise NotImplementedError()
74
+
75
+
76
+ class GdsApp(GdsBaseFunction):
77
+ """ GDS start-up process functionality
78
+
79
+ A pluggable base class used to start a new process as part of the GDS command line invocation. This allows
80
+ developers to add process-isolated functionality to the GDS network.
81
+
82
+ Plugin developers are required to implement the `get_process_invocation` function that returns a list of arguments
83
+ needed to invoke the process via python's `subprocess`. Additionally, the developer must define the
84
+ `register_gds_function_plugin` class method annotated with the @gds_plugin_implementation annotation.
85
+
86
+ Standard plug-in functions (get_name, get_arguments) are available should the implementer desire these features.
87
+ Arguments will be supplied to the class's `__init__` function.
88
+ """
89
+ def __init__(self, **arguments):
90
+ """ Construct the communication applications around the arguments
91
+
92
+ Command line arguments are passed in to match those returned from the `get_arguments` functions.
93
+
94
+ Args:
95
+ arguments: arguments from the command line
96
+ """
97
+ self.process = None
98
+ self.arguments = arguments
99
+
100
+ def run(self):
101
+ """ Run the application as an isolated process
102
+
103
+ GdsFunction objects require an implementation of the `run` command. This implementation will take the arguments
104
+ provided from `get_process_invocation` function and supplies them as an invocation of the isolated subprocess.
105
+ """
106
+ invocation_arguments = self.get_process_invocation()
107
+ self.process = subprocess.Popen(invocation_arguments)
108
+
109
+ def wait(self, timeout=None):
110
+ """ Wait for the app to complete then return the return code
111
+
112
+ Waits (blocking) for the process to complete. Then returns the return code of the underlying process. If timeout
113
+ is non-None then the process will be killed after waiting for the timeout and another wait of timeout will be
114
+ allowed for the killed process to exit.
115
+
116
+ Return:
117
+ return code of the underlying process
118
+ """
119
+ try:
120
+ _, _ = self.process.wait(timeout=timeout)
121
+ except subprocess.TimeoutExpired:
122
+ self.process.kill()
123
+ _, _ = self.process.wait(timeout=timeout)
124
+ return self.process.returncode
125
+
126
+ @abstractmethod
127
+ def get_process_invocation(self) -> List[str]:
128
+ """ Run the start-up function
129
+
130
+ Run the start-up function unconstrained by the limitations of running in a dedicated subprocess.
131
+
132
+ """
133
+ raise NotImplementedError()
134
+
135
+ @classmethod
136
+ @gds_plugin_specification
137
+ def register_gds_app_plugin(cls) -> Type["GdsApp"]:
138
+ """Register a gds start-up application
139
+
140
+ Plugin hook for registering a plugin that supplies start-up functionality. This functionality will run on start-up
141
+ of the GDS network isolated into a dedicated process.
142
+
143
+ Note: users should return the class, not an instance of the class. Needed arguments for instantiation are
144
+ determined from class methods, solicited via the command line, and provided at construction time to the chosen
145
+ instantiation.
146
+
147
+ Returns:
148
+ GdsApp subclass
149
+ """
150
+ raise NotImplementedError()