flwr-nightly 1.21.0.dev20250808__py3-none-any.whl → 1.21.0.dev20250811__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.
flwr/common/constant.py CHANGED
@@ -259,3 +259,21 @@ class EventLogWriterType:
259
259
  def __new__(cls) -> EventLogWriterType:
260
260
  """Prevent instantiation."""
261
261
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
262
+
263
+
264
+ class ExecPluginType:
265
+ """SuperExec plugin types."""
266
+
267
+ CLIENT_APP = "clientapp"
268
+ SERVER_APP = "serverapp"
269
+ SIMULATION = "simulation"
270
+
271
+ def __new__(cls) -> ExecPluginType:
272
+ """Prevent instantiation."""
273
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
274
+
275
+ @staticmethod
276
+ def all() -> list[str]:
277
+ """Return all SuperExec plugin types."""
278
+ # Filter all constants (uppercase) of the class
279
+ return [v for k, v in vars(ExecPluginType).items() if k.isupper()]
flwr/common/telemetry.py CHANGED
@@ -181,6 +181,10 @@ class EventType(str, Enum):
181
181
  RUN_SUPERNODE_ENTER = auto()
182
182
  RUN_SUPERNODE_LEAVE = auto()
183
183
 
184
+ # CLI: `flower-superexec`
185
+ RUN_SUPEREXEC_ENTER = auto()
186
+ RUN_SUPEREXEC_LEAVE = auto()
187
+
184
188
 
185
189
  # Use the ThreadPoolExecutor with max_workers=1 to have a queue
186
190
  # and also ensure that telemetry calls are not blocking.
@@ -20,7 +20,6 @@ import gc
20
20
  from logging import DEBUG, ERROR, INFO
21
21
  from pathlib import Path
22
22
  from queue import Queue
23
- from time import sleep
24
23
  from typing import Optional
25
24
 
26
25
  from flwr.cli.config_utils import get_fab_metadata
@@ -64,6 +63,7 @@ from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
64
63
  from flwr.proto.run_pb2 import UpdateRunStatusRequest # pylint: disable=E0611
65
64
  from flwr.server.grid.grpc_grid import GrpcGrid
66
65
  from flwr.server.run_serverapp import run as run_
66
+ from flwr.supercore.app_utils import simple_get_token, start_parent_process_monitor
67
67
 
68
68
 
69
69
  def flwr_serverapp() -> None:
@@ -91,23 +91,31 @@ def flwr_serverapp() -> None:
91
91
  run_serverapp(
92
92
  serverappio_api_address=args.serverappio_api_address,
93
93
  log_queue=log_queue,
94
+ token=args.token,
94
95
  run_once=args.run_once,
95
96
  flwr_dir=args.flwr_dir,
96
97
  certificates=None,
98
+ parent_pid=args.parent_pid,
97
99
  )
98
100
 
99
101
  # Restore stdout/stderr
100
102
  restore_output()
101
103
 
102
104
 
103
- def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
105
+ def run_serverapp( # pylint: disable=R0913, R0914, R0915, R0917, W0212
104
106
  serverappio_api_address: str,
105
107
  log_queue: Queue[Optional[str]],
106
108
  run_once: bool,
109
+ token: Optional[str] = None,
107
110
  flwr_dir: Optional[str] = None,
108
111
  certificates: Optional[bytes] = None,
112
+ parent_pid: Optional[int] = None,
109
113
  ) -> None:
110
114
  """Run Flower ServerApp process."""
115
+ # Monitor the main process in case of SIGKILL
116
+ if parent_pid is not None:
117
+ start_parent_process_monitor(parent_pid)
118
+
111
119
  # Resolve directory where FABs are installed
112
120
  flwr_dir_ = get_flwr_dir(flwr_dir)
113
121
  log_uploader = None
@@ -126,15 +134,15 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
126
134
  root_certificates=certificates,
127
135
  )
128
136
 
137
+ # If token is not set, loop until token is received from SuperLink
138
+ if token is None:
139
+ log(DEBUG, "[flwr-serverapp] Request token")
140
+ token = simple_get_token(grid._stub)
141
+
129
142
  # Pull ServerAppInputs from LinkState
130
- req = PullAppInputsRequest()
143
+ req = PullAppInputsRequest(token=token)
131
144
  log(DEBUG, "[flwr-serverapp] Pull ServerAppInputs")
132
145
  res: PullAppInputsResponse = grid._stub.PullAppInputs(req)
133
- if not res.HasField("run"):
134
- sleep(3)
135
- run_status = None
136
- continue
137
-
138
146
  context = context_from_proto(res.context)
139
147
  run = run_from_proto(res.run)
140
148
  fab = fab_from_proto(res.fab)
@@ -209,7 +217,9 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
209
217
  # Send resulting context
210
218
  context_proto = context_to_proto(updated_context)
211
219
  log(DEBUG, "[flwr-serverapp] Will push ServerAppOutputs")
212
- out_req = PushAppOutputsRequest(run_id=run.run_id, context=context_proto)
220
+ out_req = PushAppOutputsRequest(
221
+ token=token, run_id=run.run_id, context=context_proto
222
+ )
213
223
  _ = grid._stub.PushAppOutputs(out_req)
214
224
 
215
225
  run_status = RunStatus(Status.FINISHED, SubStatus.COMPLETED, "")
@@ -250,8 +260,9 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
250
260
  if grid:
251
261
  grid.close()
252
262
 
253
- # Clean up the Context
263
+ # Clean up the Context and the token
254
264
  context = None
265
+ token = None
255
266
  gc.collect()
256
267
 
257
268
  event(
@@ -276,6 +287,19 @@ def _parse_args_run_flwr_serverapp() -> argparse.ArgumentParser:
276
287
  help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name)."
277
288
  f"By default, it is set to {SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
278
289
  )
290
+ parser.add_argument(
291
+ "--token",
292
+ type=str,
293
+ required=False,
294
+ help="Unique token generated by SuperNode for each ServerApp execution",
295
+ )
296
+ parser.add_argument(
297
+ "--parent-pid",
298
+ type=int,
299
+ default=None,
300
+ help="The PID of the parent process. When set, the process will terminate "
301
+ "when the parent process exits.",
302
+ )
279
303
  parser.add_argument(
280
304
  "--run-once",
281
305
  action="store_true",
@@ -329,14 +329,11 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
329
329
  # Init access to LinkState
330
330
  state = self.state_factory.state()
331
331
 
332
+ # Validate the token
333
+ run_id = self._verify_token(request.token, context)
334
+
332
335
  # Lock access to LinkState, preventing obtaining the same pending run_id
333
336
  with self.lock:
334
- # Attempt getting the run_id of a pending run
335
- run_id = state.get_pending_run_id()
336
- # If there's no pending run, return an empty response
337
- if run_id is None:
338
- return PullAppInputsResponse()
339
-
340
337
  # Init access to Ffs
341
338
  ffs = self.ffs_factory.ffs()
342
339
 
@@ -367,6 +364,9 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
367
364
  """Push ServerApp process outputs."""
368
365
  log(DEBUG, "ServerAppIoServicer.PushAppOutputs")
369
366
 
367
+ # Validate the token
368
+ run_id = self._verify_token(request.token, context)
369
+
370
370
  # Init state and store
371
371
  state = self.state_factory.state()
372
372
  store = self.objectstore_factory.store()
@@ -381,6 +381,9 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
381
381
  )
382
382
 
383
383
  state.set_serverapp_context(request.run_id, context_from_proto(request.context))
384
+
385
+ # Remove the token
386
+ state.delete_token(run_id)
384
387
  return PushAppOutputsResponse()
385
388
 
386
389
  def UpdateRunStatus(
@@ -548,6 +551,18 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
548
551
 
549
552
  return ConfirmMessageReceivedResponse()
550
553
 
554
+ def _verify_token(self, token: str, context: grpc.ServicerContext) -> int:
555
+ """Verify the token and return the associated run ID."""
556
+ state = self.state_factory.state()
557
+ run_id = state.get_run_id_by_token(token)
558
+ if run_id is None or not state.verify_token(run_id, token):
559
+ context.abort(
560
+ grpc.StatusCode.PERMISSION_DENIED,
561
+ "Invalid token.",
562
+ )
563
+ raise RuntimeError("This line should never be reached.")
564
+ return run_id
565
+
551
566
 
552
567
  def _raise_if(validation_error: bool, request_name: str, detail: str) -> None:
553
568
  """Raise a `ValueError` with a detailed message if a validation error occurs."""
@@ -0,0 +1,88 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Utility functions for app processes."""
16
+
17
+
18
+ import os
19
+ import signal
20
+ import threading
21
+ import time
22
+ from typing import Union
23
+
24
+ from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
25
+ ListAppsToLaunchRequest,
26
+ ListAppsToLaunchResponse,
27
+ RequestTokenRequest,
28
+ RequestTokenResponse,
29
+ )
30
+ from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
31
+ from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub
32
+
33
+ if os.name == "nt":
34
+ from ctypes import windll # type: ignore
35
+
36
+
37
+ def _pid_exists(pid: int) -> bool:
38
+ """Check if a process with the given PID exists.
39
+
40
+ This works on Unix-like systems and Windows.
41
+ """
42
+ # Use `ctypes` to check if the process exists on Windows
43
+ if os.name == "nt":
44
+ handle = windll.kernel32.OpenProcess(0x1000, False, pid)
45
+ if handle:
46
+ windll.kernel32.CloseHandle(handle)
47
+ return True
48
+ return False
49
+ # Use `os.kill` on Unix-like systems
50
+ try:
51
+ os.kill(pid, 0)
52
+ except OSError:
53
+ return False
54
+ return True
55
+
56
+
57
+ def start_parent_process_monitor(
58
+ parent_pid: int,
59
+ ) -> None:
60
+ """Monitor the parent process and exit if it terminates."""
61
+
62
+ def monitor() -> None:
63
+ while True:
64
+ time.sleep(0.2)
65
+ if not _pid_exists(parent_pid):
66
+ # This works on Unix-like systems and Windows
67
+ # Avoid `os.kill` on Windows
68
+ signal.raise_signal(signal.SIGTERM)
69
+
70
+ threading.Thread(target=monitor, daemon=True).start()
71
+
72
+
73
+ def simple_get_token(stub: Union[ClientAppIoStub, ServerAppIoStub]) -> str:
74
+ """Get a token from SuperLink/SuperNode.
75
+
76
+ This shall be removed once the SuperExec is fully implemented.
77
+ """
78
+ while True:
79
+ res: ListAppsToLaunchResponse = stub.ListAppsToLaunch(ListAppsToLaunchRequest())
80
+
81
+ for run_id in res.run_ids:
82
+ tk_res: RequestTokenResponse = stub.RequestToken(
83
+ RequestTokenRequest(run_id=run_id)
84
+ )
85
+ if tk_res.token:
86
+ return tk_res.token
87
+
88
+ time.sleep(1) # Wait before retrying to get run IDs
@@ -0,0 +1,22 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower command line interface for shared infrastructure components."""
16
+
17
+
18
+ from .flower_superexec import flower_superexec
19
+
20
+ __all__ = [
21
+ "flower_superexec",
22
+ ]
@@ -0,0 +1,110 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """`flower-superexec` command."""
16
+
17
+
18
+ import argparse
19
+ from logging import INFO
20
+
21
+ from flwr.common import EventType, event
22
+ from flwr.common.constant import ExecPluginType
23
+ from flwr.common.logger import log
24
+ from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
25
+ from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub
26
+ from flwr.proto.simulationio_pb2_grpc import SimulationIoStub
27
+ from flwr.supercore.superexec.plugin import (
28
+ ClientAppExecPlugin,
29
+ ExecPlugin,
30
+ ServerAppExecPlugin,
31
+ SimulationExecPlugin,
32
+ )
33
+ from flwr.supercore.superexec.run_superexec import run_superexec
34
+
35
+
36
+ def flower_superexec() -> None:
37
+ """Run `flower-superexec` command."""
38
+ args = _parse_args().parse_args()
39
+
40
+ # Log the first message after parsing arguments in case of `--help`
41
+ log(INFO, "Starting Flower SuperExec")
42
+
43
+ # Trigger telemetry event
44
+ event(EventType.RUN_SUPEREXEC_ENTER, {"plugin_type": args.plugin_type})
45
+
46
+ # Get the plugin class and stub class based on the plugin type
47
+ plugin_class, stub_class = _get_plugin_and_stub_class(args.plugin_type)
48
+ run_superexec(
49
+ plugin_class=plugin_class,
50
+ stub_class=stub_class, # type: ignore
51
+ appio_api_address=args.appio_api_address,
52
+ flwr_dir=args.flwr_dir,
53
+ parent_pid=args.parent_pid,
54
+ )
55
+
56
+
57
+ def _parse_args() -> argparse.ArgumentParser:
58
+ """Parse `flower-superexec` command line arguments."""
59
+ parser = argparse.ArgumentParser(
60
+ description="Run Flower SuperExec.",
61
+ )
62
+ parser.add_argument(
63
+ "--appio-api-address", type=str, required=True, help="Address of the AppIO API"
64
+ )
65
+ parser.add_argument(
66
+ "--plugin-type",
67
+ type=str,
68
+ choices=ExecPluginType.all(),
69
+ required=True,
70
+ help="The type of plugin to use.",
71
+ )
72
+ parser.add_argument(
73
+ "--insecure",
74
+ action="store_true",
75
+ help="Connect to the AppIO API without TLS. "
76
+ "Data transmitted between the client and server is not encrypted. "
77
+ "Use this flag only if you understand the risks.",
78
+ )
79
+ parser.add_argument(
80
+ "--flwr-dir",
81
+ default=None,
82
+ help="""The path containing installed Flower Apps.
83
+ By default, this value is equal to:
84
+
85
+ - `$FLWR_HOME/` if `$FLWR_HOME` is defined
86
+ - `$XDG_DATA_HOME/.flwr/` if `$XDG_DATA_HOME` is defined
87
+ - `$HOME/.flwr/` in all other cases
88
+ """,
89
+ )
90
+ parser.add_argument(
91
+ "--parent-pid",
92
+ type=int,
93
+ default=None,
94
+ help="The PID of the parent process. When set, the process will terminate "
95
+ "when the parent process exits.",
96
+ )
97
+ return parser
98
+
99
+
100
+ def _get_plugin_and_stub_class(
101
+ plugin_type: str,
102
+ ) -> tuple[type[ExecPlugin], type[object]]:
103
+ """Get the plugin class and stub class based on the plugin type."""
104
+ if plugin_type == ExecPluginType.CLIENT_APP:
105
+ return ClientAppExecPlugin, ClientAppIoStub
106
+ if plugin_type == ExecPluginType.SERVER_APP:
107
+ return ServerAppExecPlugin, ServerAppIoStub
108
+ if plugin_type == ExecPluginType.SIMULATION:
109
+ return SimulationExecPlugin, SimulationIoStub
110
+ raise ValueError(f"Unknown plugin type: {plugin_type}")
@@ -17,8 +17,12 @@
17
17
 
18
18
  from .clientapp_exec_plugin import ClientAppExecPlugin
19
19
  from .exec_plugin import ExecPlugin
20
+ from .serverapp_exec_plugin import ServerAppExecPlugin
21
+ from .simulation_exec_plugin import SimulationExecPlugin
20
22
 
21
23
  __all__ = [
22
24
  "ClientAppExecPlugin",
23
25
  "ExecPlugin",
26
+ "ServerAppExecPlugin",
27
+ "SimulationExecPlugin",
24
28
  ]
@@ -0,0 +1,53 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Simple base Flower SuperExec plugin for app processes."""
16
+
17
+
18
+ import os
19
+ import subprocess
20
+ from collections.abc import Sequence
21
+ from typing import Optional
22
+
23
+ from .exec_plugin import ExecPlugin
24
+
25
+
26
+ class BaseExecPlugin(ExecPlugin):
27
+ """Simple Flower SuperExec plugin for app processes.
28
+
29
+ The plugin always selects the first candidate run ID.
30
+ """
31
+
32
+ # Placeholders to be defined in subclasses
33
+ command = ""
34
+ appio_api_address_arg = ""
35
+
36
+ def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
37
+ """Select a run ID to execute from a sequence of candidates."""
38
+ if not candidate_run_ids:
39
+ return None
40
+ return candidate_run_ids[0]
41
+
42
+ def launch_app(self, token: str, run_id: int) -> None:
43
+ """Launch the application associated with a given run ID and token."""
44
+ cmds = [self.command, "--insecure"]
45
+ cmds += [self.appio_api_address_arg, self.appio_api_address]
46
+ cmds += ["--token", token]
47
+ cmds += ["--parent-pid", str(os.getpid())]
48
+ cmds += ["--flwr-dir", self.flwr_dir]
49
+ # Launch the client app without waiting for it to complete.
50
+ # Since we don't need to manage the process, we intentionally avoid using
51
+ # a `with` statement. Suppress the pylint warning for it in this case.
52
+ # pylint: disable-next=consider-using-with
53
+ subprocess.Popen(cmds)
@@ -15,35 +15,14 @@
15
15
  """Simple Flower SuperExec plugin for ClientApp."""
16
16
 
17
17
 
18
- import os
19
- import subprocess
20
- from collections.abc import Sequence
21
- from typing import Optional
18
+ from .base_exec_plugin import BaseExecPlugin
22
19
 
23
- from .exec_plugin import ExecPlugin
24
20
 
25
-
26
- class ClientAppExecPlugin(ExecPlugin):
21
+ class ClientAppExecPlugin(BaseExecPlugin):
27
22
  """Simple Flower SuperExec plugin for ClientApp.
28
23
 
29
24
  The plugin always selects the first candidate run ID.
30
25
  """
31
26
 
32
- def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
33
- """Select a run ID to execute from a sequence of candidates."""
34
- if not candidate_run_ids:
35
- return None
36
- return candidate_run_ids[0]
37
-
38
- def launch_app(self, token: str, run_id: int) -> None:
39
- """Launch the application associated with a given run ID and token."""
40
- cmds = ["flwr-clientapp", "--insecure"]
41
- cmds += ["--clientappio-api-address", self.appio_api_address]
42
- cmds += ["--token", token]
43
- cmds += ["--parent-pid", str(os.getpid())]
44
- cmds += ["--flwr-dir", self.flwr_dir]
45
- # Launch the client app without waiting for it to complete.
46
- # Since we don't need to manage the process, we intentionally avoid using
47
- # a `with` statement. Suppress the pylint warning for it in this case.
48
- # pylint: disable-next=consider-using-with
49
- subprocess.Popen(cmds)
27
+ command = "flwr-clientapp"
28
+ appio_api_address_arg = "--clientappio-api-address"
@@ -0,0 +1,28 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Simple Flower SuperExec plugin for ServerApp."""
16
+
17
+
18
+ from .base_exec_plugin import BaseExecPlugin
19
+
20
+
21
+ class ServerAppExecPlugin(BaseExecPlugin):
22
+ """Simple Flower SuperExec plugin for ServerApp.
23
+
24
+ The plugin always selects the first candidate run ID.
25
+ """
26
+
27
+ command = "flwr-serverapp"
28
+ appio_api_address_arg = "--serverappio-api-address"
@@ -0,0 +1,28 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Simple Flower SuperExec plugin for simulation processes."""
16
+
17
+
18
+ from .base_exec_plugin import BaseExecPlugin
19
+
20
+
21
+ class SimulationExecPlugin(BaseExecPlugin):
22
+ """Simple Flower SuperExec plugin for simulation processes.
23
+
24
+ The plugin always selects the first candidate run ID.
25
+ """
26
+
27
+ command = "flwr-simulation"
28
+ appio_api_address_arg = "--simulationio-api-address"
@@ -0,0 +1,122 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower SuperExec."""
16
+
17
+
18
+ import time
19
+ from typing import Optional
20
+
21
+ from flwr.common.config import get_flwr_dir
22
+ from flwr.common.exit_handlers import register_exit_handlers
23
+ from flwr.common.grpc import create_channel, on_channel_state_change
24
+ from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
25
+ from flwr.common.serde import run_from_proto
26
+ from flwr.common.telemetry import EventType
27
+ from flwr.common.typing import Run
28
+ from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
29
+ ListAppsToLaunchRequest,
30
+ RequestTokenRequest,
31
+ )
32
+ from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
33
+ from flwr.proto.run_pb2 import GetRunRequest # pylint: disable=E0611
34
+ from flwr.supercore.app_utils import start_parent_process_monitor
35
+
36
+ from .plugin import ExecPlugin
37
+
38
+
39
+ def run_superexec(
40
+ plugin_class: type[ExecPlugin],
41
+ stub_class: type[ClientAppIoStub],
42
+ appio_api_address: str,
43
+ flwr_dir: Optional[str] = None,
44
+ parent_pid: Optional[int] = None,
45
+ ) -> None:
46
+ """Run Flower SuperExec.
47
+
48
+ Parameters
49
+ ----------
50
+ plugin_class : type[ExecPlugin]
51
+ The class of the SuperExec plugin to use.
52
+ stub_class : type[ClientAppIoStub]
53
+ The gRPC stub class for the AppIO API.
54
+ appio_api_address : str
55
+ The address of the AppIO API.
56
+ flwr_dir : Optional[str] (default: None)
57
+ The Flower directory.
58
+ parent_pid : Optional[int] (default: None)
59
+ The PID of the parent process. If provided, the SuperExec will terminate
60
+ when the parent process exits.
61
+ """
62
+ # Start monitoring the parent process if a PID is provided
63
+ if parent_pid is not None:
64
+ start_parent_process_monitor(parent_pid)
65
+
66
+ # Create the channel to the AppIO API
67
+ # No TLS support for now, so insecure connection
68
+ channel = create_channel(
69
+ server_address=appio_api_address,
70
+ insecure=True,
71
+ root_certificates=None,
72
+ )
73
+ channel.subscribe(on_channel_state_change)
74
+
75
+ # Register exit handlers to close the channel on exit
76
+ register_exit_handlers(
77
+ event_type=EventType.RUN_SUPEREXEC_LEAVE,
78
+ exit_message="SuperExec terminated gracefully.",
79
+ exit_handlers=[lambda: channel.close()], # pylint: disable=W0108
80
+ )
81
+
82
+ # Create the gRPC stub for the AppIO API
83
+ stub = stub_class(channel)
84
+ _wrap_stub(stub, _make_simple_grpc_retry_invoker())
85
+
86
+ def get_run(run_id: int) -> Run:
87
+ _req = GetRunRequest(run_id=run_id)
88
+ _res = stub.GetRun(_req)
89
+ return run_from_proto(_res.run)
90
+
91
+ # Create the SuperExec plugin instance
92
+ plugin = plugin_class(
93
+ appio_api_address=appio_api_address,
94
+ flwr_dir=str(get_flwr_dir(flwr_dir)),
95
+ get_run=get_run,
96
+ )
97
+
98
+ # Start the main loop
99
+ try:
100
+ while True:
101
+ # Fetch suitable run IDs
102
+ ls_req = ListAppsToLaunchRequest()
103
+ ls_res = stub.ListAppsToLaunch(ls_req)
104
+
105
+ # Allow the plugin to select a run ID
106
+ run_id = None
107
+ if ls_res.run_ids:
108
+ run_id = plugin.select_run_id(candidate_run_ids=ls_res.run_ids)
109
+
110
+ # Apply for a token if a run ID was selected
111
+ if run_id is not None:
112
+ tk_req = RequestTokenRequest(run_id=run_id)
113
+ tk_res = stub.RequestToken(tk_req)
114
+
115
+ # Launch the app if a token was granted; do nothing if not
116
+ if tk_res.token:
117
+ plugin.launch_app(token=tk_res.token, run_id=run_id)
118
+
119
+ # Sleep for a while before checking again
120
+ time.sleep(1)
121
+ finally:
122
+ channel.close()
@@ -16,9 +16,6 @@
16
16
 
17
17
 
18
18
  import gc
19
- import os
20
- import threading
21
- import time
22
19
  from logging import DEBUG, ERROR, INFO
23
20
  from typing import Optional
24
21
 
@@ -55,8 +52,6 @@ from flwr.common.serde import (
55
52
  )
56
53
  from flwr.common.typing import Fab, Run
57
54
  from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
58
- ListAppsToLaunchRequest,
59
- ListAppsToLaunchResponse,
60
55
  PullAppInputsRequest,
61
56
  PullAppInputsResponse,
62
57
  PullAppMessagesRequest,
@@ -64,11 +59,10 @@ from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
64
59
  PushAppMessagesRequest,
65
60
  PushAppOutputsRequest,
66
61
  PushAppOutputsResponse,
67
- RequestTokenRequest,
68
- RequestTokenResponse,
69
62
  )
70
63
  from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
71
64
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
65
+ from flwr.supercore.app_utils import simple_get_token, start_parent_process_monitor
72
66
  from flwr.supercore.utils import mask_string
73
67
 
74
68
 
@@ -101,7 +95,8 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
101
95
  while True:
102
96
  # If token is not set, loop until token is received from SuperNode
103
97
  if token is None:
104
- token = get_token(stub)
98
+ log(DEBUG, "[flwr-clientapp] Request token")
99
+ token = simple_get_token(stub)
105
100
 
106
101
  # Pull Message, Context, Run and (optional) FAB from SuperNode
107
102
  message, context, run, fab = pull_clientappinputs(stub=stub, token=token)
@@ -173,36 +168,6 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
173
168
  channel.close()
174
169
 
175
170
 
176
- def start_parent_process_monitor(
177
- parent_pid: int,
178
- ) -> None:
179
- """Monitor the parent process and exit if it terminates."""
180
-
181
- def monitor() -> None:
182
- while True:
183
- time.sleep(0.2)
184
- if os.getppid() != parent_pid:
185
- os.kill(os.getpid(), 9)
186
-
187
- threading.Thread(target=monitor, daemon=True).start()
188
-
189
-
190
- def get_token(stub: ClientAppIoStub) -> str:
191
- """Get a token from SuperNode."""
192
- log(DEBUG, "[flwr-clientapp] Request token")
193
- while True:
194
- res: ListAppsToLaunchResponse = stub.ListAppsToLaunch(ListAppsToLaunchRequest())
195
-
196
- for run_id in res.run_ids:
197
- tk_res: RequestTokenResponse = stub.RequestToken(
198
- RequestTokenRequest(run_id=run_id)
199
- )
200
- if tk_res.token:
201
- return tk_res.token
202
-
203
- time.sleep(1) # Wait before retrying to get run IDs
204
-
205
-
206
171
  def pull_clientappinputs(
207
172
  stub: ClientAppIoStub, token: str
208
173
  ) -> tuple[Message, Context, Run, Optional[Fab]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flwr-nightly
3
- Version: 1.21.0.dev20250808
3
+ Version: 1.21.0.dev20250811
4
4
  Summary: Flower: A Friendly Federated AI Framework
5
5
  License: Apache-2.0
6
6
  Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
@@ -108,7 +108,7 @@ flwr/common/args.py,sha256=XFQ5PU0lU7NS1QCiKhhESHVeL8KSjcD3x8h4P3e5qlM,5298
108
108
  flwr/common/auth_plugin/__init__.py,sha256=DktrRcGZrRarLf7Jb_UlHtOyLp9_-kEplyq6PS5-vOA,988
109
109
  flwr/common/auth_plugin/auth_plugin.py,sha256=mM7SuphO4OsVAVJR1GErYVgYT83ZjxDzS_gha12bT9E,4855
110
110
  flwr/common/config.py,sha256=glcZDjco-amw1YfQcYTFJ4S1pt9APoexT-mf1QscuHs,13960
111
- flwr/common/constant.py,sha256=IMjx9E9iwQrbBuzsXtu0u-baFGENcWoyxU0caIvtSx0,8250
111
+ flwr/common/constant.py,sha256=0w5GCaW078GMj_VMQX-Lb7OQDnA70GmWZMzufDvGv6E,8769
112
112
  flwr/common/context.py,sha256=Be8obQR_OvEDy1OmshuUKxGRQ7Qx89mf5F4xlhkR10s,2407
113
113
  flwr/common/date.py,sha256=1ZT2cRSpC2DJqprOVTLXYCR_O2_OZR0zXO_brJ3LqWc,1554
114
114
  flwr/common/differential_privacy.py,sha256=FdlpdpPl_H_2HJa8CQM1iCUGBBQ5Dc8CzxmHERM-EoE,6148
@@ -151,7 +151,7 @@ flwr/common/secure_aggregation/secaggplus_constants.py,sha256=dGYhWOBMMDJcQH4_tQ
151
151
  flwr/common/secure_aggregation/secaggplus_utils.py,sha256=E_xU-Zd45daO1em7M6C2wOjFXVtJf-6tl7fp-7xq1wo,3214
152
152
  flwr/common/serde.py,sha256=dx66gumR1BpwK45qDwvDLd_05VoKfzkUzWZ7zuZi1-0,21960
153
153
  flwr/common/serde_utils.py,sha256=krx2C_W31KpfmDqnDCtULoTkT8WKweWTJ7FHYWtF1r4,5815
154
- flwr/common/telemetry.py,sha256=jF47v0SbnBd43XamHtl3wKxs3knFUY2p77cm_2lzZ8M,8762
154
+ flwr/common/telemetry.py,sha256=xfx3KLFQKy0Qx8P7MAsOQxr5J3sEAQF5Kr5J-4jPz-o,8859
155
155
  flwr/common/typing.py,sha256=B8z50fv8K0H4F5m8XRPBoWy-qqbNMyQXBAaaSuzfbnY,6936
156
156
  flwr/common/version.py,sha256=7GAGzPn73Mkh09qhrjbmjZQtcqVhBuzhFBaK4Mk4VRk,1325
157
157
  flwr/compat/__init__.py,sha256=gbfDQKKKMZzi3GswyVRgyLdDlHiWj3wU6dg7y6m5O_s,752
@@ -250,7 +250,7 @@ flwr/server/server.py,sha256=39m4FSN2T-uVA-no9nstN0eWW0co-IUUAIMmpd3V7Jc,17893
250
250
  flwr/server/server_app.py,sha256=8uagoZX-3CY3tazPqkIV9jY-cN0YrRRrDmVe23o0AV0,9515
251
251
  flwr/server/server_config.py,sha256=e_6ddh0riwOJsdNn2BFev344uMWfDk9n7dyjNpPgm1w,1349
252
252
  flwr/server/serverapp/__init__.py,sha256=xcC0T_MQSMS9cicUzUUpMNCOsF2d8Oh_8jvnoBLuZvo,800
253
- flwr/server/serverapp/app.py,sha256=k0wAdZiMAEz7WsUigxv038I5Eel3vXwuSqGGj2HtX3c,9524
253
+ flwr/server/serverapp/app.py,sha256=-IZY02Ym6AREMPaLld6WCD9mjDz7wsqfYgLLJxpy2pg,10433
254
254
  flwr/server/serverapp_components.py,sha256=dfSqmrsVy3arKXpl3ZIBQWdV8rehfIms8aJooyzdmEM,2118
255
255
  flwr/server/strategy/__init__.py,sha256=HhsSWMWaC7oCb2g7Kqn1MBKdrfvgi8VxACy9ZL706Q0,2836
256
256
  flwr/server/strategy/aggregate.py,sha256=smlKKy-uFUuuFR12vlclucnwSQWRz78R79-Km4RWqbw,13978
@@ -305,7 +305,7 @@ flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=NIJ1BDcj54TUpNaeb23af
305
305
  flwr/server/superlink/linkstate/utils.py,sha256=IeLh7iGRCHU5MEWOl7iriaSE4L__8GWOa2OleXadK5M,15444
306
306
  flwr/server/superlink/serverappio/__init__.py,sha256=Fy4zJuoccZe5mZSEIpOmQvU6YeXFBa1M4eZuXXmJcn8,717
307
307
  flwr/server/superlink/serverappio/serverappio_grpc.py,sha256=zcvzDhCAnlFxAwCiJUHNm6IE7-rk5jeZqSmPgjEY3AU,2307
308
- flwr/server/superlink/serverappio/serverappio_servicer.py,sha256=hNTpzbBAUBlMB-PgPWW-DlVqfsmM_whXsXQeC8ZpNG8,19579
308
+ flwr/server/superlink/serverappio/serverappio_servicer.py,sha256=3C_0boRbYuY1Vlf0DRGzBvTUX-D5UUzxYkFihSMZf-A,20094
309
309
  flwr/server/superlink/simulation/__init__.py,sha256=Ry8DrNaZCMcQXvUc4FoCN2m3dvUQgWjasfp015o3Ec4,718
310
310
  flwr/server/superlink/simulation/simulationio_grpc.py,sha256=VqWKxjpd4bCgPFKsgtIZPk9YcG0kc1EEmr5k20EKty4,2205
311
311
  flwr/server/superlink/simulation/simulationio_servicer.py,sha256=m1T1zvEn81jlfx9hVTqmeWxAu6APCS2YW8l5O0OQvhU,7724
@@ -331,6 +331,9 @@ flwr/simulation/ray_transport/utils.py,sha256=KrexpWYCF-dAF3UHc9yDbPQWO-ahMT-BbD
331
331
  flwr/simulation/run_simulation.py,sha256=-sp3dNZcp7MCAH0BlmZpVcFAGvozRdYXRdDYcH_2Zxk,20838
332
332
  flwr/simulation/simulationio_connection.py,sha256=mzS1C6EEREwQDPceDo30anAasmTDLb9qqV2tXlBhOUA,3494
333
333
  flwr/supercore/__init__.py,sha256=pqkFoow_E6UhbBlhmoD1gmTH-33yJRhBsIZqxRPFZ7U,755
334
+ flwr/supercore/app_utils.py,sha256=WS3tly_QIWE-NRogbtFVC5l6arxP3Md1XItI9idmt0M,2771
335
+ flwr/supercore/cli/__init__.py,sha256=EDl2aO-fuQfxSbL-T1W9RAfA2N0hpWHmqX_GSwblJbQ,845
336
+ flwr/supercore/cli/flower_superexec.py,sha256=J_rf7SCVW9L9wsBScOYa-oJOpyb_e1WOtwTGSyUFu1k,3882
334
337
  flwr/supercore/corestate/__init__.py,sha256=Vau6-L_JG5QzNqtCTa9xCKGGljc09wY8avZmIjSJemg,774
335
338
  flwr/supercore/corestate/corestate.py,sha256=rDAWWeG5DcpCyQso9Z3RhwL4zr2IroPlRMcDzqoSu8s,2328
336
339
  flwr/supercore/ffs/__init__.py,sha256=U3KXwG_SplEvchat27K0LYPoPHzh-cwwT_NHsGlYMt8,908
@@ -347,9 +350,13 @@ flwr/supercore/object_store/object_store.py,sha256=J-rI3X7ET-F6dqOyM-UfHKCCQtPJ_
347
350
  flwr/supercore/object_store/object_store_factory.py,sha256=QVwE2ywi7vsj2iKfvWWnNw3N_I7Rz91NUt2RpcbJ7iM,1527
348
351
  flwr/supercore/object_store/utils.py,sha256=DcPbrb9PenloAPoQRiKiXX9DrDfpXcSyY0cZpgn4PeQ,1680
349
352
  flwr/supercore/superexec/__init__.py,sha256=XKX208hZ6a9gZ4KT9kMqfpCtp_8VGxekzKFfHSu2esQ,707
350
- flwr/supercore/superexec/plugin/__init__.py,sha256=OH5WYxZssOUzOVya3na4yHH0bRwtYmnottUreJT9R20,868
351
- flwr/supercore/superexec/plugin/clientapp_exec_plugin.py,sha256=qX7vcB2n4crlfTTHKNqV0jHBmk8DiCIRuGDwLGAhP7g,1975
353
+ flwr/supercore/superexec/plugin/__init__.py,sha256=GNwq8uNdE8RB7ywEFRAvKjLFzgS3YXgz39-HBGsemWw,1035
354
+ flwr/supercore/superexec/plugin/base_exec_plugin.py,sha256=fL-Ufc9Dp56OhWOzNSJUc7HumbkuSDYqZKwde2opG4g,2074
355
+ flwr/supercore/superexec/plugin/clientapp_exec_plugin.py,sha256=9FT6ufEqV5K9g4FaAB9lVDbIv-VCH5LcxT4YKy23roE,1035
352
356
  flwr/supercore/superexec/plugin/exec_plugin.py,sha256=w3jmtxdv7ov_EdAgifKcm4q8nV39e2Xna4sNjqClwOM,2447
357
+ flwr/supercore/superexec/plugin/serverapp_exec_plugin.py,sha256=IwRzdPV-cSKwrP2krGh0De4IkAuxsmgK0WU6J-2GXqM,1035
358
+ flwr/supercore/superexec/plugin/simulation_exec_plugin.py,sha256=upn5zE-YKkl_jTw8RzmeyQ58PU_UAlQ7CqnBXXdng8I,1060
359
+ flwr/supercore/superexec/run_superexec.py,sha256=2-W6UfPgdzEpHSGOvqkIr5OAhD3Zb_O4quk8ZU2oISw,4288
353
360
  flwr/supercore/utils.py,sha256=ebuHMbeA8eXisX0oMPqBK3hk7uVnIE_yiqWVz8YbkpQ,1324
354
361
  flwr/superexec/__init__.py,sha256=YFqER0IJc1XEWfsX6AxZ9LSRq0sawPYrNYki-brvTIc,715
355
362
  flwr/superexec/deployment.py,sha256=CEgWfkN_lH6Vci03RjwKLENw2z6kxNvUdVEErPbqYDY,830
@@ -376,12 +383,12 @@ flwr/supernode/nodestate/in_memory_nodestate.py,sha256=rr_tg7YXhf_seYFipSB59TAfh
376
383
  flwr/supernode/nodestate/nodestate.py,sha256=jCOewZyctecMxsM0-_-pQwef9P3O5QjnKCgCGyx2PK4,5047
377
384
  flwr/supernode/nodestate/nodestate_factory.py,sha256=UYTDCcwK_baHUmkzkJDxL0UEqvtTfOMlQRrROMCd0Xo,1430
378
385
  flwr/supernode/runtime/__init__.py,sha256=JQdqd2EMTn-ORMeTvewYYh52ls0YKP68jrps1qioxu4,718
379
- flwr/supernode/runtime/run_clientapp.py,sha256=efocWcTe2ufkhe4BzMGgxe0COq9aDyJ6HzXwQ8ZaIAU,10551
386
+ flwr/supernode/runtime/run_clientapp.py,sha256=vAeBTgIi4SmV4IRq1dSjXaxrFUPEeHg-nkvt5iAZ1Zc,9675
380
387
  flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca8gxdEo,717
381
388
  flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
382
389
  flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=nIHRu38EWK-rpNOkcgBRAAKwYQQWFeCwu0lkO7OPZGQ,10239
383
390
  flwr/supernode/start_client_internal.py,sha256=iqJR8WbCW-8RQIRNwARZYoxhnlaAo5KnluCOEfRoLWM,21020
384
- flwr_nightly-1.21.0.dev20250808.dist-info/METADATA,sha256=Vum_8HABoCD2VNWP0z2uHa-Zq8mrknTMJllovrrQPF0,15966
385
- flwr_nightly-1.21.0.dev20250808.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
386
- flwr_nightly-1.21.0.dev20250808.dist-info/entry_points.txt,sha256=jNpDXGBGgs21RqUxelF_jwGaxtqFwm-MQyfz-ZqSjrA,367
387
- flwr_nightly-1.21.0.dev20250808.dist-info/RECORD,,
391
+ flwr_nightly-1.21.0.dev20250811.dist-info/METADATA,sha256=oiloqTG58PFA094Vp2CmxyqFyCdrQTN74woSpLvclHA,15966
392
+ flwr_nightly-1.21.0.dev20250811.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
393
+ flwr_nightly-1.21.0.dev20250811.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
394
+ flwr_nightly-1.21.0.dev20250811.dist-info/RECORD,,
@@ -1,5 +1,6 @@
1
1
  [console_scripts]
2
2
  flower-simulation=flwr.simulation.run_simulation:run_simulation_from_cli
3
+ flower-superexec=flwr.supercore.cli:flower_superexec
3
4
  flower-superlink=flwr.server.app:run_superlink
4
5
  flower-supernode=flwr.supernode.cli:flower_supernode
5
6
  flwr=flwr.cli.app:app