shellphone 0.0.1__tar.gz → 0.0.2__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.
- {shellphone-0.0.1 → shellphone-0.0.2}/.gitignore +1 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/PKG-INFO +1 -1
- {shellphone-0.0.1 → shellphone-0.0.2}/pyproject.toml +1 -1
- {shellphone-0.0.1 → shellphone-0.0.2}/room.py +4 -1
- {shellphone-0.0.1 → shellphone-0.0.2}/shellphone/cli.py +16 -6
- {shellphone-0.0.1 → shellphone-0.0.2}/tests/test_cli.py +44 -11
- {shellphone-0.0.1 → shellphone-0.0.2}/LICENSE +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/connector.py +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/dist/assets/index-BSKIXO0A.css +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/dist/assets/index-Bk3QCeVl.js +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/dist/index.html +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/icon-180.png +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/icon-512.png +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/manifest.webmanifest +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/room-icon.png +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/shellphone/README.md +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/shellphone/__init__.py +0 -0
- {shellphone-0.0.1 → shellphone-0.0.2}/sw.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: shellphone
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: A chat room for you and your coding agents, on your own computer.
|
|
5
5
|
Project-URL: Homepage, https://github.com/woltspace/shellphone
|
|
6
6
|
Project-URL: Issues, https://github.com/woltspace/shellphone/issues
|
|
@@ -793,7 +793,10 @@ def code_ref():
|
|
|
793
793
|
"""The commit this room runs, so strangers' agents fetch exactly this version (not a branch that can move)."""
|
|
794
794
|
if os.environ.get("SHELLPHONE_REF"):
|
|
795
795
|
return os.environ["SHELLPHONE_REF"]
|
|
796
|
-
|
|
796
|
+
try:
|
|
797
|
+
got = subprocess.run(["git", "-C", str(HERE), "rev-parse", "HEAD"], capture_output=True, text=True)
|
|
798
|
+
except OSError: # no git on this computer
|
|
799
|
+
return "main"
|
|
797
800
|
return got.stdout.strip() if got.returncode == 0 else "main"
|
|
798
801
|
|
|
799
802
|
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
HERE = Path(__file__).resolve().parent
|
|
13
13
|
PREFIX = "shellphone-room-"
|
|
14
|
+
THROWAWAY = "throwaway.pid" # only in a room made to be deleted: the pid of the command that owns it
|
|
14
15
|
HOST_EMAIL = "host@shellphone.local" # the room keys people by email; a local room has one person, the host
|
|
15
16
|
CONNECTOR = ("wait", "post", "history", "working", "run", "voice", "version")
|
|
16
17
|
|
|
@@ -48,10 +49,12 @@ def alive(pid):
|
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
def sweep():
|
|
51
|
-
"""A room that crashed couldn't delete itself: remove what such rooms left behind.
|
|
52
|
+
"""A throwaway room that crashed couldn't delete itself: remove what such rooms left behind. Only folders this
|
|
53
|
+
command made as throwaway carry the marker; a kept room never does, wherever it is and whatever it's called.
|
|
54
|
+
Rooms still open are left alone."""
|
|
52
55
|
for d in Path(tempfile.gettempdir()).glob(PREFIX + "*"):
|
|
53
56
|
try:
|
|
54
|
-
pid = int((d /
|
|
57
|
+
pid = int((d / THROWAWAY).read_text())
|
|
55
58
|
except (OSError, ValueError):
|
|
56
59
|
continue # not clearly ours to delete
|
|
57
60
|
if not alive(pid):
|
|
@@ -77,10 +80,12 @@ def room(a):
|
|
|
77
80
|
os.chmod(data, 0o700)
|
|
78
81
|
else:
|
|
79
82
|
data = Path(tempfile.mkdtemp(prefix=PREFIX)) # mode 700
|
|
80
|
-
|
|
83
|
+
(data / THROWAWAY).write_text(str(os.getpid()))
|
|
81
84
|
port = free_port(a.port)
|
|
82
85
|
url = f"http://127.0.0.1:{port}"
|
|
83
86
|
os.environ.update(SHELLPHONE_DATA=str(data), SHELLPHONE_URL=url) # read by room.py when it loads
|
|
87
|
+
if (HERE / "room.py").is_file(): # installed as a package: there's no checkout to ask git about (and maybe no git)
|
|
88
|
+
os.environ.setdefault("SHELLPHONE_REF", "main")
|
|
84
89
|
r = part("room")
|
|
85
90
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
86
91
|
r.add_person(HOST_EMAIL, a.name)
|
|
@@ -92,9 +97,15 @@ def room(a):
|
|
|
92
97
|
server = r.http.server.ThreadingHTTPServer(("127.0.0.1", port), r.Handler)
|
|
93
98
|
server.daemon_threads = True
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
closing = []
|
|
101
|
+
|
|
102
|
+
def stop(*_): # the first stop signal closes the room; any that follow do nothing, so closing always finishes
|
|
103
|
+
if not closing:
|
|
104
|
+
closing.append(True)
|
|
105
|
+
raise KeyboardInterrupt
|
|
97
106
|
signal.signal(signal.SIGTERM, stop)
|
|
107
|
+
if signal.getsignal(signal.SIGINT) is not signal.SIG_IGN: # started in the background, Ctrl-C isn't ours to take
|
|
108
|
+
signal.signal(signal.SIGINT, stop)
|
|
98
109
|
print(f"""Shell Phone room is open, on this computer only.
|
|
99
110
|
|
|
100
111
|
Open the chat: {url}/?k={token}
|
|
@@ -111,7 +122,6 @@ Press Ctrl-C to close the room. """ + (f"It's kept in {data}." if a.keep else "E
|
|
|
111
122
|
if r.CON is not None:
|
|
112
123
|
r.CON.close()
|
|
113
124
|
if a.keep:
|
|
114
|
-
(data / "host.pid").unlink(missing_ok=True)
|
|
115
125
|
print(f"\nRoom closed. It's kept in {data}.")
|
|
116
126
|
else:
|
|
117
127
|
shutil.rmtree(data, ignore_errors=True)
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
"""The front door, end to end on one computer: open a room, log in with the link, bring an agent, talk, close.
|
|
2
2
|
|
|
3
|
-
python3 -m unittest discover tests
|
|
3
|
+
python3 -m unittest discover tests # the code in this folder
|
|
4
|
+
SHELLPHONE_CLI=/some/venv/bin/shellphone python3 -m unittest discover tests # an installed package, e.g. a built wheel
|
|
4
5
|
"""
|
|
5
|
-
import http.cookiejar, json, os, re, signal, subprocess, sys, tempfile, time, unittest, urllib.error, urllib.request
|
|
6
|
+
import http.cookiejar, json, os, re, shlex, signal, subprocess, sys, tempfile, time, unittest, urllib.error, urllib.request
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
REPO = Path(__file__).resolve().parent.parent
|
|
9
10
|
sys.path.insert(0, str(REPO))
|
|
10
|
-
|
|
11
|
+
INSTALLED = os.environ.get("SHELLPHONE_CLI") # test this command (an installed package) instead of the code in this folder
|
|
12
|
+
CLI = shlex.split(INSTALLED) if INSTALLED else [sys.executable, "-m", "shellphone.cli"]
|
|
13
|
+
ENV = {**os.environ, "PYTHONUNBUFFERED": "1", **({} if INSTALLED else {"PYTHONPATH": str(REPO)})}
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
def run(*args, cwd, stdin=None):
|
|
14
|
-
return subprocess.run([*CLI, *args], cwd=cwd, input=stdin, capture_output=True, text=True, timeout=30,
|
|
15
|
-
env={**os.environ, "PYTHONPATH": str(REPO)})
|
|
17
|
+
return subprocess.run([*CLI, *args], cwd=cwd, input=stdin, capture_output=True, text=True, timeout=30, env=ENV)
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class Room:
|
|
19
21
|
"""A room in its own process, as `shellphone room` starts it."""
|
|
20
|
-
def __init__(self, *args):
|
|
21
|
-
self.proc = subprocess.Popen([*CLI, "room", "--port", "0", *args], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
|
22
|
-
env={**os.environ, "PYTHONPATH": str(REPO), "PYTHONUNBUFFERED": "1"})
|
|
22
|
+
def __init__(self, *args, env=None):
|
|
23
|
+
self.proc = subprocess.Popen([*CLI, "room", "--port", "0", *args], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env or ENV)
|
|
23
24
|
self.banner = ""
|
|
24
25
|
while "Press Ctrl-C" not in self.banner:
|
|
25
26
|
line = self.proc.stdout.readline()
|
|
@@ -59,14 +60,14 @@ class FrontDoor(unittest.TestCase):
|
|
|
59
60
|
r.proc.communicate()
|
|
60
61
|
self.tmp.cleanup()
|
|
61
62
|
|
|
62
|
-
def room(self, *args):
|
|
63
|
-
self.rooms.append(Room(*args))
|
|
63
|
+
def room(self, *args, **kw):
|
|
64
|
+
self.rooms.append(Room(*args, **kw))
|
|
64
65
|
return self.rooms[-1]
|
|
65
66
|
|
|
66
67
|
def data_dir(self, room):
|
|
67
68
|
"""Where the room keeps its data, found from outside: the temp folder holding this room's pid."""
|
|
68
69
|
for d in Path(tempfile.gettempdir()).glob("shellphone-room-*"):
|
|
69
|
-
if (d / "
|
|
70
|
+
if (d / "throwaway.pid").is_file() and (d / "throwaway.pid").read_text() == str(room.proc.pid):
|
|
70
71
|
return d
|
|
71
72
|
|
|
72
73
|
def test_the_link_logs_the_host_in_and_nothing_else_does(self):
|
|
@@ -142,6 +143,11 @@ class FrontDoor(unittest.TestCase):
|
|
|
142
143
|
self.assertNotEqual(got.returncode, 0, link)
|
|
143
144
|
self.assertFalse((work / "shellphone").exists(), link)
|
|
144
145
|
|
|
146
|
+
def test_a_room_opens_on_a_computer_with_no_git(self):
|
|
147
|
+
r = self.room(env={**ENV, "PATH": self.tmp.name}) # nothing on the path at all
|
|
148
|
+
self.assertEqual(r.browser.open(r.open).status, 200)
|
|
149
|
+
self.assertIn("Room closed and deleted.", r.close())
|
|
150
|
+
|
|
145
151
|
def test_closing_deletes_the_room(self):
|
|
146
152
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
147
153
|
r = self.room()
|
|
@@ -151,6 +157,18 @@ class FrontDoor(unittest.TestCase):
|
|
|
151
157
|
self.assertIn("Room closed and deleted.", r.close(sig))
|
|
152
158
|
self.assertFalse(data.exists())
|
|
153
159
|
|
|
160
|
+
def test_a_second_stop_signal_cant_interrupt_the_closing(self):
|
|
161
|
+
for burst in ([signal.SIGINT] * 6, [signal.SIGTERM] * 6, [signal.SIGTERM, signal.SIGINT] * 3):
|
|
162
|
+
r = self.room()
|
|
163
|
+
r.browser.open(r.open)
|
|
164
|
+
data = self.data_dir(r)
|
|
165
|
+
for sig in burst: # a double Ctrl-C, or a stop sent to a whole process group
|
|
166
|
+
r.proc.send_signal(sig)
|
|
167
|
+
out = r.proc.communicate(timeout=10)[0]
|
|
168
|
+
self.assertIn("Room closed and deleted.", out)
|
|
169
|
+
self.assertNotIn("Traceback", out)
|
|
170
|
+
self.assertFalse(data.exists())
|
|
171
|
+
|
|
154
172
|
def test_keep_keeps_the_room_and_changes_the_links(self):
|
|
155
173
|
kept = Path(self.tmp.name) / "kept"
|
|
156
174
|
r = self.room("--keep", str(kept))
|
|
@@ -166,6 +184,21 @@ class FrontDoor(unittest.TestCase):
|
|
|
166
184
|
stale = again.url + "/#" + old.split("#")[1] # last run's agent link no longer opens the room
|
|
167
185
|
self.assertNotEqual(run("join", stale, "--name", "helper", cwd=Path(self.tmp.name)).returncode, 0)
|
|
168
186
|
|
|
187
|
+
def test_a_kept_room_is_never_swept_even_when_it_looks_like_a_throwaway_one(self):
|
|
188
|
+
kept = Path(tempfile.mkdtemp(prefix="shellphone-room-saved-")) # the worst place and name to keep a room
|
|
189
|
+
self.addCleanup(__import__("shutil").rmtree, kept, True)
|
|
190
|
+
r = self.room("--keep", str(kept))
|
|
191
|
+
r.browser.open(r.open)
|
|
192
|
+
r.browser.open(urllib.request.Request(r.url + "/api/messages", data=json.dumps({"text": "keep me"}).encode(),
|
|
193
|
+
headers={"Content-Type": "application/json"}))
|
|
194
|
+
r.proc.kill() # a crash: nothing gets tidied
|
|
195
|
+
r.proc.communicate()
|
|
196
|
+
self.room() # another room starts and sweeps
|
|
197
|
+
self.assertTrue((kept / "room.db").is_file())
|
|
198
|
+
again = self.room("--keep", str(kept)) # and the kept room opens again with what was said
|
|
199
|
+
again.browser.open(again.open)
|
|
200
|
+
self.assertIn("keep me", [m.get("text") for m in again.state()["messages"]])
|
|
201
|
+
|
|
169
202
|
def test_a_crashed_rooms_leftovers_are_swept_and_an_open_room_is_left_alone(self):
|
|
170
203
|
open_room = self.room()
|
|
171
204
|
crashed = self.room()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|