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.
- amigent/__init__.py +3 -0
- amigent/amiga/amigent-repair +65 -0
- amigent/amiga/amigent-repair.info +0 -0
- amigent/amiga/amigent-scan +9 -0
- amigent/amiga/bin/Amigent.info +0 -0
- amigent/amiga/bin/amigentd +0 -0
- amigent/amiga/bin/amigentd.000 +0 -0
- amigent/amiga/bin/amigentd.info +0 -0
- amigent/amiga/menu-add.rexx +45 -0
- amigent/amigactl.py +199 -0
- amigent/amigentd.py +466 -0
- amigent/channel.py +411 -0
- amigent/client.py +26 -0
- amigent/desktop.py +308 -0
- amigent/doctor.py +70 -0
- amigent/errors.py +11 -0
- amigent/files.py +236 -0
- amigent/fleet.py +252 -0
- amigent/inventory.py +37 -0
- amigent/legacy.py +90 -0
- amigent/locks.py +44 -0
- amigent/lzh.py +175 -0
- amigent/machines.py +126 -0
- amigent/macos/Amigent.swift +76 -0
- amigent/macos/Amigent.zip +0 -0
- amigent/net.py +29 -0
- amigent/paths.py +18 -0
- amigent/pictures.py +104 -0
- amigent/platform_util.py +22 -0
- amigent/power.py +66 -0
- amigent/roster.py +163 -0
- amigent/secret.py +73 -0
- amigent/serve.py +84 -0
- amigent/server.py +723 -0
- amigent/setup.py +177 -0
- amigent/tls.py +150 -0
- amigent/transfer.py +64 -0
- amigent/viewer.py +911 -0
- amigent/watch.py +71 -0
- amigent/web/amigent.icns +0 -0
- amigent/web/amigent.ico +0 -0
- amigent/web/fleet.css +22 -0
- amigent/web/fleet.html +29 -0
- amigent/web/fleet.js +158 -0
- amigent/web/icon.png +0 -0
- amigent/web/icon.svg +15 -0
- amigent/web/index.html +246 -0
- amigent/web/locales/cs.json +129 -0
- amigent/web/locales/da.json +129 -0
- amigent/web/locales/de.json +129 -0
- amigent/web/locales/en.json +129 -0
- amigent/web/locales/es.json +129 -0
- amigent/web/locales/fi.json +129 -0
- amigent/web/locales/fr.json +129 -0
- amigent/web/locales/hu.json +129 -0
- amigent/web/locales/it.json +129 -0
- amigent/web/locales/pl.json +129 -0
- amigent/web/locales/sk.json +129 -0
- amigent/web/locales/sv.json +129 -0
- amigent/web/viewer.css +448 -0
- amigent/web/viewer.js +939 -0
- amigent-1.0.0.dist-info/METADATA +138 -0
- amigent-1.0.0.dist-info/RECORD +67 -0
- amigent-1.0.0.dist-info/WHEEL +5 -0
- amigent-1.0.0.dist-info/entry_points.txt +6 -0
- amigent-1.0.0.dist-info/licenses/LICENSE +21 -0
- amigent-1.0.0.dist-info/top_level.txt +1 -0
amigent/__init__.py
ADDED
|
@@ -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
|
+
]
|