amigent 1.0.0__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.
Files changed (67) hide show
  1. amigent/__init__.py +3 -0
  2. amigent/amiga/amigent-repair +65 -0
  3. amigent/amiga/amigent-repair.info +0 -0
  4. amigent/amiga/amigent-scan +9 -0
  5. amigent/amiga/bin/Amigent.info +0 -0
  6. amigent/amiga/bin/amigentd +0 -0
  7. amigent/amiga/bin/amigentd.000 +0 -0
  8. amigent/amiga/bin/amigentd.info +0 -0
  9. amigent/amiga/menu-add.rexx +45 -0
  10. amigent/amigactl.py +199 -0
  11. amigent/amigentd.py +466 -0
  12. amigent/channel.py +411 -0
  13. amigent/client.py +26 -0
  14. amigent/desktop.py +308 -0
  15. amigent/doctor.py +70 -0
  16. amigent/errors.py +11 -0
  17. amigent/files.py +236 -0
  18. amigent/fleet.py +252 -0
  19. amigent/inventory.py +37 -0
  20. amigent/legacy.py +90 -0
  21. amigent/locks.py +44 -0
  22. amigent/lzh.py +175 -0
  23. amigent/machines.py +126 -0
  24. amigent/macos/Amigent.swift +76 -0
  25. amigent/macos/Amigent.zip +0 -0
  26. amigent/net.py +29 -0
  27. amigent/paths.py +18 -0
  28. amigent/pictures.py +104 -0
  29. amigent/platform_util.py +22 -0
  30. amigent/power.py +66 -0
  31. amigent/roster.py +163 -0
  32. amigent/secret.py +73 -0
  33. amigent/serve.py +84 -0
  34. amigent/server.py +723 -0
  35. amigent/setup.py +177 -0
  36. amigent/tls.py +150 -0
  37. amigent/transfer.py +64 -0
  38. amigent/viewer.py +911 -0
  39. amigent/watch.py +71 -0
  40. amigent/web/amigent.icns +0 -0
  41. amigent/web/amigent.ico +0 -0
  42. amigent/web/fleet.css +22 -0
  43. amigent/web/fleet.html +29 -0
  44. amigent/web/fleet.js +158 -0
  45. amigent/web/icon.png +0 -0
  46. amigent/web/icon.svg +15 -0
  47. amigent/web/index.html +246 -0
  48. amigent/web/locales/cs.json +129 -0
  49. amigent/web/locales/da.json +129 -0
  50. amigent/web/locales/de.json +129 -0
  51. amigent/web/locales/en.json +129 -0
  52. amigent/web/locales/es.json +129 -0
  53. amigent/web/locales/fi.json +129 -0
  54. amigent/web/locales/fr.json +129 -0
  55. amigent/web/locales/hu.json +129 -0
  56. amigent/web/locales/it.json +129 -0
  57. amigent/web/locales/pl.json +129 -0
  58. amigent/web/locales/sk.json +129 -0
  59. amigent/web/locales/sv.json +129 -0
  60. amigent/web/viewer.css +448 -0
  61. amigent/web/viewer.js +939 -0
  62. amigent-1.0.0.dist-info/METADATA +138 -0
  63. amigent-1.0.0.dist-info/RECORD +67 -0
  64. amigent-1.0.0.dist-info/WHEEL +5 -0
  65. amigent-1.0.0.dist-info/entry_points.txt +6 -0
  66. amigent-1.0.0.dist-info/licenses/LICENSE +21 -0
  67. amigent-1.0.0.dist-info/top_level.txt +1 -0
amigent/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Amigent: complete remote control for a real Amiga, by AI or manually."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,65 @@
1
+ ; amigent-repair: restores Amigent after a system update. Safe to run twice.
2
+ FailAt 21
3
+ ; amigent-setup puts its folder here (SYS:amigent unless --home says otherwise)
4
+ Set SRC SYS:amigent
5
+ ; amigactld only where an older setup installed it
6
+ If EXISTS $SRC/amigactld
7
+ If NOT EXISTS C:amigactld
8
+ Copy $SRC/amigactld C: CLONE
9
+ Protect C:amigactld rwed
10
+ EndIf
11
+ EndIf
12
+ If NOT EXISTS C:amigentd
13
+ Copy $SRC/amigentd C: CLONE
14
+ Protect C:amigentd rwed
15
+ EndIf
16
+ If NOT EXISTS S:amigent.key
17
+ If EXISTS $SRC/amigent.key
18
+ Copy $SRC/amigent.key S: CLONE
19
+ EndIf
20
+ EndIf
21
+ If NOT EXISTS S:amigent-tls.pem
22
+ If EXISTS $SRC/amigent-tls.pem
23
+ Copy $SRC/amigent-tls.pem S: CLONE
24
+ EndIf
25
+ EndIf
26
+ If NOT EXISTS S:amigactld.conf
27
+ If EXISTS $SRC/amigactld.conf
28
+ Copy $SRC/amigactld.conf S: CLONE
29
+ EndIf
30
+ EndIf
31
+ Delete >NIL: S:amigent-start QUIET
32
+ If EXISTS $SRC/amigactld
33
+ Search >NIL: S:User-Startup "C:amigactld" QUIET
34
+ If WARN
35
+ Echo >>S:User-Startup "RUN >NIL: C:amigactld"
36
+ EndIf
37
+ EndIf
38
+ Search >NIL: S:User-Startup "C:amigentd" QUIET
39
+ If WARN
40
+ If EXISTS $SRC/amigent.tlsonly
41
+ Echo >>S:User-Startup "RUN >NIL: C:amigentd TLSONLY"
42
+ Else
43
+ Echo >>S:User-Startup "RUN >NIL: C:amigentd"
44
+ EndIf
45
+ EndIf
46
+ If EXISTS C:amigactld
47
+ Status >NIL: COMMAND=C:amigactld
48
+ If WARN
49
+ Run >NIL: C:amigactld
50
+ EndIf
51
+ EndIf
52
+ Status >NIL: COMMAND=C:amigentd
53
+ If WARN
54
+ Run >NIL: C:amigentd
55
+ EndIf
56
+ ; every installed program brings its own module
57
+ List $SRC/repair.d PAT=#?.repair LFORMAT "Execute %p%n" >T:amigent-modules
58
+ Execute T:amigent-modules
59
+ Delete >NIL: T:amigent-modules QUIET
60
+ Search >NIL: S:Startup-Sequence "S:User-Startup" QUIET
61
+ If WARN
62
+ RequestChoice >NIL: "Amigent" "Startup-Sequence no longer runs S:User-Startup.*NPlease tell the AI agent." "OK"
63
+ Else
64
+ RequestChoice >NIL: "Amigent" "Amigent is repaired.*NPlease restart the Amiga." "OK"
65
+ EndIf
Binary file
@@ -0,0 +1,9 @@
1
+ ; amigent-scan: inventory of programs and their $VER strings.
2
+ ; Writes RAM:amigent-inventory.txt, then RAM:amigent-scan.done
3
+ FailAt 21
4
+ Delete >NIL: RAM:amigent-inventory.txt RAM:amigent-scan.script RAM:amigent-scan.done QUIET
5
+ Echo >RAM:amigent-scan.script "FailAt 21"
6
+ ChangeTaskPri -5
7
+ List >>RAM:amigent-scan.script SYS:C SYS:L SYS:Libs SYS:Classes SYS:Devs SYS:Expansion SYS:Internet SYS:Prefs SYS:Rexxc SYS:Storage SYS:System SYS:Tools SYS:Utilities SYS:WBStartup ALL FILES PAT=~(#?.(info|guide|txt|doc|readme|pdf|html|htm|png|jpg|jpeg|gif|iff|ilbm|mod|wav|mp3|h|c|i|s|asm|fd|cat|catalog|font|pfb|ttf|cfg|prefs|lha|lzx|zip)) LFORMAT "Echo >>RAM:amigent-inventory.txt *"@%p%n|%l*"*NVersion >>RAM:amigent-inventory.txt *"%p%n*""
8
+ Execute >NIL: RAM:amigent-scan.script
9
+ Echo >RAM:amigent-scan.done "done"
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,45 @@
1
+ /* Inserts an entry into the AmiStart menu file, sorted by name.
2
+ Args: SECTION|Name|Program[|Stack]. AmiStart shows it after its next start.
3
+ The file is handled as one string because READLN splits lines over 1000 chars. */
4
+ parse arg section '|' name '|' file '|' stack
5
+ if stack = '' then stack = 32768
6
+ prefs = 'SYS:Utilities/Expansion/AmiStart/sm.prefs'
7
+ temp = 'T:sm.prefs.new'
8
+ nl = '0a'x
9
+ item = 'ITEM ICON="'file'" NAME="'name'" METHOD="0" CMDMODE="0" RGB="0x0" TEXTRGB="0x0" FILE="'file'" EXECMODE="0" STACK="'stack'"'
10
+
11
+ if ~open('in', prefs, 'R') then exit 10
12
+ data = readch('in', 65535)
13
+ call close('in')
14
+ if length(data) > 65000 then exit 10
15
+
16
+ start = index(data, nl'NEWDIR NAME="'section'"')
17
+ if start = 0 then exit 10
18
+ stop = index(data, nl'ENDDIR', start)
19
+ if stop = 0 then exit 10
20
+
21
+ insert = stop + 1
22
+ p = index(data, nl, start + 1) + 1
23
+ do while p < stop
24
+ e = index(data, nl, p)
25
+ line = substr(data, p, e - p)
26
+ if left(line, 5) == 'ITEM ' & index(line, '" GROUP ') = 0 then do
27
+ parse var line 'NAME="' current '"'
28
+ if upper(current) > upper(name) then do
29
+ insert = p
30
+ leave
31
+ end
32
+ end
33
+ p = e + 1
34
+ end
35
+
36
+ if ~open('out', temp, 'W') then exit 10
37
+ call writech('out', left(data, insert - 1) || item || nl || substr(data, insert))
38
+ call close('out')
39
+
40
+ do try = 1 to 10
41
+ address command 'Copy' temp prefs 'QUIET'
42
+ if rc = 0 then exit 0
43
+ address command 'Wait 1'
44
+ end
45
+ exit 10
amigent/amigactl.py ADDED
@@ -0,0 +1,199 @@
1
+ """Client for the amigactld wire protocol: files, commands, system state. The old-school path.
2
+
3
+ Clean-room job from the published spec (docs/protocol.md and
4
+ docs/protocol-commands.md in the amigactl project), not a rip of its client code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import socket
10
+
11
+ from .client import ExecResult, VolumeFlush, join_path
12
+ from .errors import AmigaError
13
+ from .roster import address
14
+
15
+ __all__ = ["AmigaCtl", "AmigaError", "ExecResult", "join_path"] # the old addresses still deliver
16
+
17
+ ENCODING = "iso-8859-1"
18
+ CHUNK_SIZE = 4096
19
+ MAX_LINE = 4096
20
+
21
+
22
+ class AmigaCtl(VolumeFlush):
23
+ def __init__(self, host: str, port: int = 6800, timeout: float = 30.0):
24
+ self.host = host
25
+ self.port = port
26
+ self.timeout = timeout
27
+ self._sock: socket.socket | None = None
28
+ self._buf = b""
29
+ self.banner = ""
30
+
31
+ def connect(self) -> None:
32
+ self.close()
33
+ try:
34
+ self._sock = socket.create_connection((address(self.host), self.port), self.timeout)
35
+ self.banner = self._line()
36
+ except OSError as e:
37
+ self.close()
38
+ raise AmigaError(f"cannot reach amigactld at {self.host}:{self.port}: {e}") from e
39
+ if not self.banner.startswith("AMIGACTL "):
40
+ banner, self.banner = self.banner, ""
41
+ self.close()
42
+ raise AmigaError(f"unexpected banner: {banner!r}")
43
+
44
+ def close(self) -> None:
45
+ if self._sock:
46
+ self._sock.close()
47
+ self._sock = None
48
+ self._buf = b""
49
+
50
+ # framing
51
+
52
+ def _recv(self) -> None:
53
+ assert self._sock
54
+ data = self._sock.recv(65536)
55
+ if not data:
56
+ raise AmigaError("connection closed by daemon")
57
+ self._buf += data
58
+
59
+ def _line(self) -> str:
60
+ while b"\n" not in self._buf:
61
+ self._recv()
62
+ raw, self._buf = self._buf.split(b"\n", 1)
63
+ return raw.rstrip(b"\r").decode(ENCODING)
64
+
65
+ def _exact(self, n: int) -> bytes:
66
+ while len(self._buf) < n:
67
+ self._recv()
68
+ data, self._buf = self._buf[:n], self._buf[n:]
69
+ return data
70
+
71
+ def _send_line(self, line: str) -> None:
72
+ raw = line.encode(ENCODING) + b"\n"
73
+ if len(raw) - 1 > MAX_LINE or "\n" in line:
74
+ raise AmigaError("command line too long or contains a newline")
75
+ if not self._sock:
76
+ self.connect()
77
+ assert self._sock
78
+ self._sock.sendall(raw)
79
+
80
+ def _status(self, line: str) -> str:
81
+ """The info field of an OK line; on ERR eat the sentinel and raise."""
82
+ if line == "OK" or line.startswith("OK "):
83
+ return line[3:]
84
+ if line.startswith("ERR "):
85
+ parts = line.split(" ", 2)
86
+ self._line() # sentinel
87
+ code = int(parts[1]) if parts[1].isdigit() else None
88
+ raise AmigaError(parts[2] if len(parts) > 2 else line, code)
89
+ raise AmigaError(f"malformed response: {line!r}")
90
+
91
+ def _text(self, command: str, *extra_lines: str) -> tuple[str, list[str]]:
92
+ """Run a command whose answer is dot-stuffed lines, SMTP style, ended by '.'."""
93
+ self._send_line(command)
94
+ for line in extra_lines:
95
+ self._send_line(line)
96
+ info = self._status(self._line())
97
+ lines = []
98
+ while True:
99
+ line = self._line()
100
+ if line == ".":
101
+ return info, lines
102
+ lines.append(line[1:] if line.startswith("..") else line)
103
+
104
+ def _binary(self, command: str, timeout: float | None = None) -> tuple[str, bytes]:
105
+ """Run a command whose answer comes in DATA chunks until END."""
106
+ self._send_line(command)
107
+ assert self._sock
108
+ self._sock.settimeout(timeout if timeout is not None else self.timeout)
109
+ try:
110
+ info = self._status(self._line())
111
+ chunks = []
112
+ while True:
113
+ line = self._line()
114
+ if line == "END":
115
+ break
116
+ if not line.startswith("DATA "):
117
+ raise AmigaError(f"malformed chunk header: {line!r}")
118
+ chunks.append(self._exact(int(line[5:])))
119
+ if self._line() != ".":
120
+ raise AmigaError("missing sentinel")
121
+ return info, b"".join(chunks)
122
+ finally:
123
+ if self._sock:
124
+ self._sock.settimeout(self.timeout)
125
+
126
+ # files
127
+
128
+ def list_dir(self, path: str, recursive: bool = False) -> list[dict]:
129
+ _, lines = self._text(f"DIR {path}" + (" RECURSIVE" if recursive else ""))
130
+ entries = []
131
+ for line in lines:
132
+ kind, name, size, protection, stamp = line.split("\t")
133
+ entries.append(
134
+ {"type": kind, "name": name, "size": int(size), "protection": protection, "date": stamp}
135
+ )
136
+ return entries
137
+
138
+ def stat(self, path: str) -> dict:
139
+ _, lines = self._text(f"STAT {path}")
140
+ return dict(line.split("=", 1) for line in lines)
141
+
142
+ def read_file(self, path: str) -> bytes:
143
+ return self._binary(f"READ {path}")[1]
144
+
145
+ def write_file(self, path: str, data: bytes, append: bool = False) -> None:
146
+ self._send_line(f"{'APPEND' if append else 'WRITE'} {path} {len(data)}")
147
+ line = self._line()
148
+ if line != "READY":
149
+ self._status(line)
150
+ raise AmigaError(f"expected READY, got {line!r}")
151
+ assert self._sock
152
+ for i in range(0, len(data), CHUNK_SIZE):
153
+ chunk = data[i : i + CHUNK_SIZE]
154
+ self._sock.sendall(f"DATA {len(chunk)}\n".encode() + chunk)
155
+ self._sock.sendall(b"END\n")
156
+ self._status(self._line())
157
+ if self._line() != ".":
158
+ raise AmigaError("missing sentinel")
159
+
160
+ def checksum(self, path: str, size: int = 0) -> int: # size: AmigaChannel uses it for patience
161
+ return int(dict(line.split("=", 1) for line in self._text(f"CHECKSUM {path}")[1])["crc32"], 16)
162
+
163
+ def delete(self, path: str) -> None:
164
+ self._text(f"DELETE {path}")
165
+
166
+ def rename(self, old: str, new: str) -> None:
167
+ self._text("RENAME", old, new)
168
+
169
+ def copy(self, source: str, dest: str) -> None:
170
+ self._text("COPY", source, dest)
171
+
172
+ def makedir(self, path: str) -> None:
173
+ self._text(f"MAKEDIR {path}")
174
+
175
+ # commands
176
+
177
+ def exec(self, command: str, cd: str | None = None, timeout: float | None = None) -> ExecResult:
178
+ """Run synchronously. The daemon is single-tasking here: everyone waits till it returns."""
179
+ prefix = f"CD={cd} " if cd else ""
180
+ info, data = self._binary(f"EXEC {prefix}{command}", timeout)
181
+ return ExecResult(int(info.split("=", 1)[1]), data.decode(ENCODING))
182
+
183
+ def exec_async(self, command: str) -> int:
184
+ """Fire and forget, no output captured; returns the daemon's process id."""
185
+ return int(self._text(f"EXEC ASYNC {command}")[0])
186
+
187
+ # system state
188
+
189
+ def sysinfo(self) -> dict:
190
+ _, lines = self._text("SYSINFO")
191
+ return dict(line.split("=", 1) for line in lines)
192
+
193
+ def volumes(self) -> list[dict]:
194
+ _, lines = self._text("VOLUMES")
195
+ keys = ("name", "used", "free", "capacity", "blocksize")
196
+ return [
197
+ {k: (v if k == "name" else int(v)) for k, v in zip(keys, line.split("\t"))}
198
+ for line in lines
199
+ ]