zettcode-broker 0.3.4__tar.gz
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.
- zettcode_broker-0.3.4/.gitignore +37 -0
- zettcode_broker-0.3.4/PKG-INFO +22 -0
- zettcode_broker-0.3.4/pyproject.toml +46 -0
- zettcode_broker-0.3.4/src/zettcode/broker/__init__.py +22 -0
- zettcode_broker-0.3.4/src/zettcode/broker/__main__.py +10 -0
- zettcode_broker-0.3.4/src/zettcode/broker/cli.py +282 -0
- zettcode_broker-0.3.4/src/zettcode/broker/client.py +837 -0
- zettcode_broker-0.3.4/src/zettcode/broker/files.py +133 -0
- zettcode_broker-0.3.4/src/zettcode/broker/finder.py +210 -0
- zettcode_broker-0.3.4/src/zettcode/broker/hub.py +964 -0
- zettcode_broker-0.3.4/src/zettcode/broker/log.py +11 -0
- zettcode_broker-0.3.4/src/zettcode/broker/models.py +132 -0
- zettcode_broker-0.3.4/src/zettcode/broker/process.py +70 -0
- zettcode_broker-0.3.4/src/zettcode/broker/protocol.py +428 -0
- zettcode_broker-0.3.4/src/zettcode/broker/py.typed +0 -0
- zettcode_broker-0.3.4/src/zettcode/broker/reload.py +94 -0
- zettcode_broker-0.3.4/src/zettcode/broker/roster.py +182 -0
- zettcode_broker-0.3.4/src/zettcode/broker/runner.py +160 -0
- zettcode_broker-0.3.4/src/zettcode/broker/server.py +183 -0
- zettcode_broker-0.3.4/src/zettcode/broker/store.py +470 -0
- zettcode_broker-0.3.4/src/zettcode/broker/text.py +81 -0
- zettcode_broker-0.3.4/src/zettcode/broker/web.py +261 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
**/.venv/
|
|
2
|
+
**/__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.smoke/
|
|
8
|
+
.coverage
|
|
9
|
+
**/.pytest_cache/
|
|
10
|
+
**/.ruff_cache/
|
|
11
|
+
.zett-tool-output/
|
|
12
|
+
.env
|
|
13
|
+
.DS_Store
|
|
14
|
+
|
|
15
|
+
# viztracer writes its trace here by default.
|
|
16
|
+
/result.json
|
|
17
|
+
|
|
18
|
+
# The docs site: dependencies, build output, and VitePress' cache.
|
|
19
|
+
node_modules/
|
|
20
|
+
docs/.vitepress/dist/
|
|
21
|
+
docs/.vitepress/cache/
|
|
22
|
+
docs/.vitepress/.temp/
|
|
23
|
+
|
|
24
|
+
# The TypeScript client is built from its sources; only dist/ is published.
|
|
25
|
+
clients/typescript/dist/
|
|
26
|
+
|
|
27
|
+
# The gateway's page is built from clients/gateway into the package when a distribution is made (`make gateway-ui`);
|
|
28
|
+
# the build output is not kept in the repository. pyproject's `artifacts` puts it in the wheel all the same.
|
|
29
|
+
packages/zettcode-gateway/src/zettcode/gateway/static/
|
|
30
|
+
|
|
31
|
+
# The palette gallery is generated by `make palettes`: every widget painted in
|
|
32
|
+
# every palette, written by docs/scripts/palette_gallery.py. Regenerate it
|
|
33
|
+
# rather than committing a 2 MB copy that goes stale the moment a colour moves.
|
|
34
|
+
docs/internal/palettes.html
|
|
35
|
+
|
|
36
|
+
# The release order is generated from the pyproject.toml files (`make order`); it is not kept in git.
|
|
37
|
+
packages/order.txt
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zettcode-broker
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: The session broker ZettCode's sessions talk through: a small loopback HTTP server and its client
|
|
5
|
+
Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
|
|
6
|
+
Author: Chang-LeHung
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: broker,sessions,zettcode
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx<1,>=0.27
|
|
19
|
+
Requires-Dist: starlette<2,>=0.48
|
|
20
|
+
Requires-Dist: uvicorn<1,>=0.35
|
|
21
|
+
Requires-Dist: websockets<17,>=15
|
|
22
|
+
Requires-Dist: zettcode-engine
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zettcode-broker"
|
|
3
|
+
description = "The session broker ZettCode's sessions talk through: a small loopback HTTP server and its client"
|
|
4
|
+
requires-python = ">=3.10"
|
|
5
|
+
dynamic = ["version"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [{name = "Chang-LeHung"}]
|
|
8
|
+
keywords = ["zettcode", "broker", "sessions"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Programming Language :: Python :: 3.10",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Programming Language :: Python :: 3.15",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"zettcode-engine",
|
|
21
|
+
"starlette>=0.48,<2",
|
|
22
|
+
"uvicorn>=0.35,<1",
|
|
23
|
+
"httpx>=0.27,<1",
|
|
24
|
+
"websockets>=15,<17",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/Chang-LeHung/zettcode"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.version]
|
|
35
|
+
# One version for the whole repository: the engine ships the file.
|
|
36
|
+
path = "../zettcode-engine/src/zettcode/version.py"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/zettcode"]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
include = ["/src", "/pyproject.toml"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""The background process that lets one ZettCode session send a message to another.
|
|
2
|
+
|
|
3
|
+
Every running session registers with one small local process, the *broker*. A session can then ask who else is
|
|
4
|
+
active, send a message to one of them, and be handed the messages sent to it; the shell turns a received message
|
|
5
|
+
into a turn, and the model gets a tool to send one. Anything else on the machine that holds the broker's token can
|
|
6
|
+
send one too.
|
|
7
|
+
|
|
8
|
+
The broker is a Starlette application served by uvicorn on a loopback port, and each part has one job:
|
|
9
|
+
|
|
10
|
+
* what is said and the limits on it — :mod:`.protocol`; who is registered — :mod:`.roster`; cleaning what comes from
|
|
11
|
+
another process — :mod:`.text`;
|
|
12
|
+
* what the broker *decides* — :mod:`.hub`: the sessions, who a message is from, whether it may pass, who is told;
|
|
13
|
+
* how that is put on the web — :mod:`.web`: the routes, the token, the WebSocket of each session;
|
|
14
|
+
* the server's life — :mod:`.server`: bind, serve, say goodbye, stop when nobody needs it; and the process around
|
|
15
|
+
it — :mod:`.runner`: the lock, the note, the signals (``python -m zettcode.broker``);
|
|
16
|
+
* what a session uses to reach it — :mod:`.client`, with :mod:`.finder` (find the broker, start it, replace an older
|
|
17
|
+
one, using :mod:`.files` and :mod:`.process`) and :mod:`.models` (the words the rest of the application uses).
|
|
18
|
+
|
|
19
|
+
The client is imported by the shell with the rest of the application, and it only connects once the first frame is
|
|
20
|
+
up; the server only ever runs as ``python -m zettcode.broker``. Nothing here imports the shell, the agent or the
|
|
21
|
+
terminal, which :mod:`tests.test_structure` holds.
|
|
22
|
+
"""
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""``zettcode broker``: start, stop, or restart the session broker by hand.
|
|
2
|
+
|
|
3
|
+
A broker is normally started by the first session that needs one and stops itself when the last session leaves, so
|
|
4
|
+
this command is for the person who wants to say so directly: after an upgrade, on a machine where a broker is stuck,
|
|
5
|
+
or before a session is opened. It decides nothing about messages; it only turns the process on and off.
|
|
6
|
+
|
|
7
|
+
The broker is found and watched by the lock it holds, never by the note alone (:mod:`zettcode.filelock`): a note can
|
|
8
|
+
outlive the process that wrote it, a held lock cannot. Stopping reuses the same ending as a broker of an older
|
|
9
|
+
protocol (:mod:`.process`), which recognises the process before touching it, asks it to stop, and waits for the lock.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from zettcode.filelock import is_held
|
|
22
|
+
|
|
23
|
+
from . import process
|
|
24
|
+
from .files import MODULE, Note, broker_dir, lock_path, log_path, read_note, remove_note
|
|
25
|
+
from .finder import spawn_broker
|
|
26
|
+
from .protocol import PROTOCOL_VERSION
|
|
27
|
+
from .reload import outdated
|
|
28
|
+
|
|
29
|
+
EXIT_OK = 0
|
|
30
|
+
EXIT_FAILED = 1
|
|
31
|
+
|
|
32
|
+
#: Seconds to wait for a broker that was just started to write the note that says where it listens.
|
|
33
|
+
START_WAIT = 8.0
|
|
34
|
+
|
|
35
|
+
#: How often the note and the lock are looked at while waiting.
|
|
36
|
+
POLL = 0.05
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Companions(ABC):
|
|
40
|
+
"""Processes that live on the broker and cannot survive its being replaced, which a restart has to deal with.
|
|
41
|
+
|
|
42
|
+
The broker knows nothing of what they are (background agents, in practice: :mod:`zettcode.daemon`); the command
|
|
43
|
+
that starts it hands one in. A new broker may speak another protocol, and a session still running the old code
|
|
44
|
+
then waits for ever, so they are stopped before the broker and started again after it.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def listing(self) -> list[str]:
|
|
49
|
+
"""Return one line for each process that is running now; empty when there is none."""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def stop(self) -> list[str]:
|
|
53
|
+
"""Stop every one of them, remember what is needed to start them again, and return a line for each."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def start(self) -> list[tuple[bool, str]]:
|
|
57
|
+
"""Start again what :meth:`stop` stopped; return, for each, whether it worked and a line to show."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
61
|
+
"""Return the parser for ``zettcode broker`` and the three things it can do."""
|
|
62
|
+
parser = argparse.ArgumentParser(
|
|
63
|
+
prog="zettcode broker",
|
|
64
|
+
description=(
|
|
65
|
+
"Start, stop, or restart the session broker: the background process that lets ZettCode sessions on this "
|
|
66
|
+
"machine message each other. A broker starts by itself when a session needs one."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"action",
|
|
71
|
+
choices=("start", "stop", "restart", "reload"),
|
|
72
|
+
help=(
|
|
73
|
+
"start a broker if there is none, stop the one there is, or do both; reload restarts it only when it is "
|
|
74
|
+
"older than this version and no background agent is on it (zettcode runs this by itself after an upgrade)"
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"-y",
|
|
79
|
+
"--yes",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="restart: do not ask before stopping and starting again the background agents running on the broker",
|
|
82
|
+
)
|
|
83
|
+
return parser
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def broker_program(argv: list[str] | None = None, companions: Companions | None = None) -> int:
|
|
87
|
+
"""Entry point of ``zettcode broker``; the exit status is what the shell reads: 0 done, 1 failed.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
argv: The arguments after ``broker``.
|
|
91
|
+
companions: What has to be stopped and started again with the broker on a ``restart``, if anything.
|
|
92
|
+
"""
|
|
93
|
+
args = build_parser().parse_args(argv)
|
|
94
|
+
directory = broker_dir()
|
|
95
|
+
if args.action == "restart":
|
|
96
|
+
return restart(directory, companions, assume_yes=args.yes)
|
|
97
|
+
if args.action == "reload":
|
|
98
|
+
return reload(directory, companions)
|
|
99
|
+
action = {"start": start, "stop": stop}[args.action]
|
|
100
|
+
return action(directory)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def reload(directory: Path, companions: Companions | None = None) -> int:
|
|
104
|
+
"""Replace the running broker when it is older than this version, and say nothing when there is nothing to do.
|
|
105
|
+
|
|
106
|
+
This is what runs behind every start of ``zettcode`` after an upgrade (:mod:`.reload`), so it never asks and never
|
|
107
|
+
stops what it should not: a broker with background agents on it is left as it is, with a line that says how to
|
|
108
|
+
restart it knowingly, because stopping them is something a person agrees to (``restart`` asks). A broker that is
|
|
109
|
+
current, newer, or not running is not touched. The status is 0 unless a replacement was tried and failed.
|
|
110
|
+
"""
|
|
111
|
+
note = outdated(directory)
|
|
112
|
+
if note is None:
|
|
113
|
+
return EXIT_OK
|
|
114
|
+
if companions is not None and (on_it := companions.listing()):
|
|
115
|
+
count = len(on_it)
|
|
116
|
+
print(
|
|
117
|
+
f"the broker (pid {note.pid}) speaks an older protocol, but {count} background "
|
|
118
|
+
f"agent{'s are' if count != 1 else ' is'} running on it, so it was left alone: "
|
|
119
|
+
"run `zettcode broker restart` when that is fine"
|
|
120
|
+
)
|
|
121
|
+
return EXIT_OK
|
|
122
|
+
# `stop` compares the note itself, in the same step that ends the broker, so what it ends is the broker that was
|
|
123
|
+
# found and never one another start of zettcode has just put in its place.
|
|
124
|
+
if stop(directory, expect=note.pid) != EXIT_OK:
|
|
125
|
+
return EXIT_FAILED
|
|
126
|
+
if running(directory):
|
|
127
|
+
# `stop` left a broker that is not the one that was found: another start replaced it first, and it is theirs.
|
|
128
|
+
return EXIT_OK
|
|
129
|
+
status = start(directory)
|
|
130
|
+
if status == EXIT_OK:
|
|
131
|
+
print(f"replaced broker {note.pid} (protocol {note.version}) with one that speaks {PROTOCOL_VERSION}")
|
|
132
|
+
return status
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def running(directory: Path) -> bool:
|
|
136
|
+
"""Return whether a broker holds its lock in ``directory``, which is the only reliable sign that one is there."""
|
|
137
|
+
return is_held(lock_path(directory))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def start(directory: Path) -> int:
|
|
141
|
+
"""Start a broker unless one is running, and say where it listens."""
|
|
142
|
+
if running(directory):
|
|
143
|
+
note = read_note(directory)
|
|
144
|
+
where = f" (pid {note.pid}, port {note.port})" if note is not None else ""
|
|
145
|
+
print(f"the broker is already running{where}")
|
|
146
|
+
return EXIT_OK
|
|
147
|
+
before = read_note(directory)
|
|
148
|
+
try:
|
|
149
|
+
spawn_broker(directory)
|
|
150
|
+
except OSError as error:
|
|
151
|
+
print(f"the broker could not be started: {error}", file=sys.stderr)
|
|
152
|
+
return EXIT_FAILED
|
|
153
|
+
note = _wait_for_note(directory, before)
|
|
154
|
+
if note is None:
|
|
155
|
+
print(f"the broker did not start; the log is {log_path(directory)}", file=sys.stderr)
|
|
156
|
+
return EXIT_FAILED
|
|
157
|
+
print(f"broker {note.pid} listening on port {note.port}")
|
|
158
|
+
return EXIT_OK
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def stop(directory: Path, expect: int | None = None) -> int:
|
|
162
|
+
"""Stop the broker that is running, and say what happened.
|
|
163
|
+
|
|
164
|
+
A note whose broker has gone is removed rather than reported: it is what the next session would read, and a
|
|
165
|
+
stale one only sends it to a port nobody is on.
|
|
166
|
+
|
|
167
|
+
An explicit ``stop`` does not compare the broker's protocol version: that rule (never end a newer broker) is for
|
|
168
|
+
a session replacing one automatically, so two versions do not end each other's in turn; a person who asks for
|
|
169
|
+
this wants it stopped, whatever it says it speaks.
|
|
170
|
+
|
|
171
|
+
With ``expect``, only the broker with that process id is ended: the note is read once, here, and a broker it does
|
|
172
|
+
not name (another start of zettcode replaced the one that was meant) is left running. That is a compare and a stop
|
|
173
|
+
in one step, which a check made before calling this cannot be.
|
|
174
|
+
"""
|
|
175
|
+
note = read_note(directory)
|
|
176
|
+
if expect is not None and (note is None or note.pid != expect):
|
|
177
|
+
return EXIT_OK
|
|
178
|
+
if not running(directory):
|
|
179
|
+
if note is not None:
|
|
180
|
+
remove_note(directory, note.token)
|
|
181
|
+
print("the broker is not running")
|
|
182
|
+
return EXIT_OK
|
|
183
|
+
if note is None:
|
|
184
|
+
print("a broker holds the lock but left no note, so it cannot be identified", file=sys.stderr)
|
|
185
|
+
return EXIT_FAILED
|
|
186
|
+
if process.end(note.pid, MODULE, released=lambda: not running(directory)):
|
|
187
|
+
print(f"stopped broker {note.pid}")
|
|
188
|
+
return EXIT_OK
|
|
189
|
+
print(f"broker {note.pid} did not stop; it was not recognised as one, or it took too long", file=sys.stderr)
|
|
190
|
+
return EXIT_FAILED
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def restart(
|
|
194
|
+
directory: Path,
|
|
195
|
+
companions: Companions | None = None,
|
|
196
|
+
*,
|
|
197
|
+
assume_yes: bool = False,
|
|
198
|
+
ask: Callable[[str], str] = input,
|
|
199
|
+
interactive: Callable[[], bool] = lambda: sys.stdin.isatty(),
|
|
200
|
+
) -> int:
|
|
201
|
+
"""Stop the broker if one is running, then start one; with ``companions`` running, only once the person agrees.
|
|
202
|
+
|
|
203
|
+
Those are stopped first and started again after the broker, because a session that outlives its broker is cut off
|
|
204
|
+
from the new one when the protocol changed. Saying no (or not being able to say anything: no terminal and no
|
|
205
|
+
``--yes``) changes nothing at all.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
directory: The broker's directory.
|
|
209
|
+
companions: What lives on the broker, or ``None`` for nothing.
|
|
210
|
+
assume_yes: Do not ask.
|
|
211
|
+
ask: Reads the answer, injectable for tests.
|
|
212
|
+
interactive: Whether anybody can be asked.
|
|
213
|
+
"""
|
|
214
|
+
running_now = companions.listing() if companions is not None else []
|
|
215
|
+
if companions is not None and running_now and not _agreed(running_now, assume_yes, ask, interactive):
|
|
216
|
+
return EXIT_FAILED
|
|
217
|
+
if companions is not None and running_now:
|
|
218
|
+
for line in companions.stop():
|
|
219
|
+
print(line)
|
|
220
|
+
if running(directory) and stop(directory) != EXIT_OK:
|
|
221
|
+
# The broker is still the old one, so what was stopped for it is brought back rather than left off.
|
|
222
|
+
return _bring_back(companions, running_now, EXIT_FAILED)
|
|
223
|
+
status = start(directory)
|
|
224
|
+
if status != EXIT_OK:
|
|
225
|
+
return _bring_back(companions, running_now, status)
|
|
226
|
+
return _bring_back(companions, running_now, EXIT_OK)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _agreed(
|
|
230
|
+
running_now: list[str], assume_yes: bool, ask: Callable[[str], str], interactive: Callable[[], bool]
|
|
231
|
+
) -> bool:
|
|
232
|
+
"""Show what a restart will stop, and return whether the person agreed to it."""
|
|
233
|
+
count = len(running_now)
|
|
234
|
+
print(f"{count} background agent{'s are' if count != 1 else ' is'} running on the broker:")
|
|
235
|
+
for line in running_now:
|
|
236
|
+
print(f" {line}")
|
|
237
|
+
print(
|
|
238
|
+
"They are stopped, and started again from their own sessions after the broker is: nothing they wrote is lost."
|
|
239
|
+
)
|
|
240
|
+
if assume_yes:
|
|
241
|
+
return True
|
|
242
|
+
if not interactive():
|
|
243
|
+
print("not restarting: nobody can be asked here. Run it in a terminal, or add --yes.", file=sys.stderr)
|
|
244
|
+
return False
|
|
245
|
+
try:
|
|
246
|
+
answer = ask("Stop them and restart the broker? [y/N] ")
|
|
247
|
+
except (EOFError, KeyboardInterrupt):
|
|
248
|
+
print()
|
|
249
|
+
return False
|
|
250
|
+
if answer.strip().lower() in ("y", "yes"):
|
|
251
|
+
return True
|
|
252
|
+
print("not restarting; nothing was changed.")
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _bring_back(companions: Companions | None, running_now: list[str], status: int) -> int:
|
|
257
|
+
"""Start again what a restart stopped and report each; the status is the broker's unless one of them failed."""
|
|
258
|
+
if companions is None or not running_now:
|
|
259
|
+
return status
|
|
260
|
+
failed = False
|
|
261
|
+
for worked, line in companions.start():
|
|
262
|
+
print(line, file=sys.stdout if worked else sys.stderr)
|
|
263
|
+
failed = failed or not worked
|
|
264
|
+
return EXIT_FAILED if failed else status
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _wait_for_note(directory: Path, before: Note | None) -> Note | None:
|
|
268
|
+
"""Wait for a broker to say where it listens: a note different from the one that was there before it started.
|
|
269
|
+
|
|
270
|
+
The token is new on every start, so a note left by a broker that died is not mistaken for the new one; another
|
|
271
|
+
session that won the race to start a broker also writes a new one, and either answer is the broker to report.
|
|
272
|
+
"""
|
|
273
|
+
deadline = time.monotonic() + START_WAIT
|
|
274
|
+
while time.monotonic() < deadline:
|
|
275
|
+
note = read_note(directory)
|
|
276
|
+
if note is not None and (before is None or note.token != before.token) and running(directory):
|
|
277
|
+
return note
|
|
278
|
+
time.sleep(POLL)
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
__all__ = ["Companions", "broker_program", "build_parser", "restart", "running", "start", "stop"]
|