Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac40afc856 | ||
|
|
ff6d474f29 | ||
|
|
44aef71813 | ||
|
|
27381e7038 | ||
|
|
21d389cc26 | ||
|
|
eae036a9fc | ||
|
|
621b5525a8 | ||
|
|
6da567aee6 | ||
|
|
7071614434 | ||
|
|
8301342728 |
+4
-2
@@ -2670,8 +2670,10 @@ class Agent:
|
|||||||
# Langlaeufer brauchen mehr als den Bridge-Default (20s): eine APK ueber
|
# Langlaeufer brauchen mehr als den Bridge-Default (20s): eine APK ueber
|
||||||
# WLAN zu installieren dauert 30-60s. Ohne das meldet ARIA "Timeout",
|
# WLAN zu installieren dauert 30-60s. Ohne das meldet ARIA "Timeout",
|
||||||
# obwohl die Installation noch laeuft.
|
# obwohl die Installation noch laeuft.
|
||||||
slow = action.startswith("adb.") or action == "ssh.exec"
|
# adb.*: Warten auf die Geraete-Autorisierung (bis 90s) + Download +
|
||||||
sat_timeout = 200.0 if slow else 20.0
|
# Installation koennen zusammen mehrere Minuten dauern.
|
||||||
|
sat_timeout = (320.0 if action.startswith("adb.")
|
||||||
|
else 200.0 if action == "ssh.exec" else 20.0)
|
||||||
result = _post("/internal/satellite", {
|
result = _post("/internal/satellite", {
|
||||||
"op": "command", "satellite": sat,
|
"op": "command", "satellite": sat,
|
||||||
"device": arguments.get("device") or "", "action": action, "params": params,
|
"device": arguments.get("device") or "", "action": action, "params": params,
|
||||||
|
|||||||
+100
-6
@@ -1605,8 +1605,9 @@
|
|||||||
<div style="color:#8888AA;margin-bottom:10px;">
|
<div style="color:#8888AA;margin-bottom:10px;">
|
||||||
Installiert die Agent-App per ADB über das Netzwerk. Voraussetzung am Gerät:
|
Installiert die Agent-App per ADB über das Netzwerk. Voraussetzung am Gerät:
|
||||||
<strong>ADB-Debugging aktiv</strong> (Fire TV: Einstellungen › Mein Fire TV ›
|
<strong>ADB-Debugging aktiv</strong> (Fire TV: Einstellungen › Mein Fire TV ›
|
||||||
Entwickleroptionen) — beim ersten Mal erscheint dort ein Dialog
|
Entwickleroptionen). Beim <strong>ersten Mal</strong> erscheint dort
|
||||||
„USB-Debugging zulassen?", den Du bestätigen musst.
|
„USB-Debugging zulassen?" — einfach bestätigen, während der Rollout läuft:
|
||||||
|
er wartet darauf und macht dann automatisch weiter.
|
||||||
</div>
|
</div>
|
||||||
<label style="display:block;margin-bottom:4px;color:#8888AA;">APK-URL</label>
|
<label style="display:block;margin-bottom:4px;color:#8888AA;">APK-URL</label>
|
||||||
<input id="apk-url" type="text" style="width:100%;padding:6px;background:#0D0D1A;border:1px solid #1E1E2E;border-radius:4px;color:#E0E0F0;"
|
<input id="apk-url" type="text" style="width:100%;padding:6px;background:#0D0D1A;border:1px solid #1E1E2E;border-radius:4px;color:#E0E0F0;"
|
||||||
@@ -1615,8 +1616,28 @@
|
|||||||
Aus dem Gitea-Release. Der Satellit lädt die Datei selbst (einmal) und
|
Aus dem Gitea-Release. Der Satellit lädt die Datei selbst (einmal) und
|
||||||
installiert sie auf dem Gerät.
|
installiert sie auf dem Gerät.
|
||||||
</div>
|
</div>
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;margin-top:12px;cursor:pointer;">
|
||||||
|
<input type="checkbox" id="apk-provision" checked>
|
||||||
|
<span>Gleich einrichten (RVS-Zugang übertragen)</span>
|
||||||
|
</label>
|
||||||
|
<div style="color:#666;font-size:11px;margin-left:26px;">
|
||||||
|
Spart das Abtippen auf dem Fernseher: Host und Token gehen direkt vom
|
||||||
|
Server an den Agenten — QR-Scan geht am Fire TV ja nicht.
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px;margin-left:26px;">
|
||||||
|
<label style="display:block;margin-bottom:4px;color:#8888AA;">Name des Geräts</label>
|
||||||
|
<input id="apk-name" type="text" style="width:100%;padding:6px;background:#0D0D1A;border:1px solid #1E1E2E;border-radius:4px;color:#E0E0F0;"
|
||||||
|
placeholder="z.B. Wohnzimmer Fire TV">
|
||||||
|
<div style="color:#666;font-size:11px;margin-top:4px;">
|
||||||
|
So spricht ARIA das Gerät an („mach einen Screenshot vom Wohnzimmer Fire TV").
|
||||||
|
Hier am PC eingeben — auf dem Fernseher wäre das Tippen mühsam.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style="margin-top:14px;">
|
<div style="margin-top:14px;">
|
||||||
<button class="btn" onclick="runApkDeploy()">📦 Jetzt ausrollen</button>
|
<button class="btn" onclick="runApkDeploy()">📦 Jetzt ausrollen</button>
|
||||||
|
<button class="btn secondary" onclick="runApkProvision()" style="margin-left:6px;"
|
||||||
|
title="Nur Zugang/Namen setzen — App bleibt wie sie ist">Nur einrichten</button>
|
||||||
|
<button class="btn secondary" onclick="runApkStatus()" style="margin-left:6px;">Status prüfen</button>
|
||||||
<span id="apk-status" style="margin-left:10px;color:#8888AA;"></span>
|
<span id="apk-status" style="margin-left:10px;color:#8888AA;"></span>
|
||||||
</div>
|
</div>
|
||||||
<pre id="apk-log" style="margin-top:12px;background:#0D0D1A;border-radius:6px;padding:10px;max-height:240px;overflow:auto;white-space:pre-wrap;color:#8888AA;font-size:12px;display:none;"></pre>
|
<pre id="apk-log" style="margin-top:12px;background:#0D0D1A;border-radius:6px;padding:10px;max-height:240px;overflow:auto;white-space:pre-wrap;color:#8888AA;font-size:12px;display:none;"></pre>
|
||||||
@@ -4706,11 +4727,22 @@
|
|||||||
'<strong style="color:#E0E0F0;">💻 ' + escapeHtml(h.name || h.hostId) + '</strong> ' +
|
'<strong style="color:#E0E0F0;">💻 ' + escapeHtml(h.name || h.hostId) + '</strong> ' +
|
||||||
'<span style="color:#666;font-size:11px;">id=' + escapeHtml(h.hostId) + '</span></div>' +
|
'<span style="color:#666;font-size:11px;">id=' + escapeHtml(h.hostId) + '</span></div>' +
|
||||||
'<button onclick="openAgentHistory(\'' + escapeHtml(h.hostId) + '\')" style="background:#12121E;color:#FFD60A;border:1px solid #FFD60A44;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;">🕘 Historie</button>' +
|
'<button onclick="openAgentHistory(\'' + escapeHtml(h.hostId) + '\')" style="background:#12121E;color:#FFD60A;border:1px solid #FFD60A44;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;">🕘 Historie</button>' +
|
||||||
|
'<button onclick="forgetHost(\'' + escapeHtml(h.hostId) + '\',\'' + escapeHtml(h.name || h.hostId) + '\')" title="Aus der Liste entfernen (z.B. alter Name nach dem Umbenennen). Meldet sich der Agent wieder, erscheint er von selbst."' +
|
||||||
|
' style="background:#12121E;color:#8888AA;border:1px solid #1E1E2E;border-radius:6px;padding:4px 9px;font-size:12px;cursor:pointer;margin-left:4px;">✕</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">' + escapeHtml(h.os || '') + ' · kann: ' + escapeHtml(caps) + ' · ' + ctrl + '</div>' +
|
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">' + escapeHtml(h.os || '') + ' · kann: ' + escapeHtml(caps) + ' · ' + ctrl + '</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
/** Entfernt einen Agenten aus der Liste (z.B. den alten Namen nach dem
|
||||||
|
* Umbenennen). Läuft er noch, meldet er sich von selbst wieder. */
|
||||||
|
function forgetHost(hostId, name) {
|
||||||
|
if (!confirm('"' + name + '" aus der Liste entfernen?\n\n' +
|
||||||
|
'Das stoppt den Agenten NICHT — läuft er noch, erscheint er ' +
|
||||||
|
'beim nächsten Lebenszeichen wieder.')) return;
|
||||||
|
send({ action: 'host_forget', hostId });
|
||||||
|
}
|
||||||
|
|
||||||
// ── APK-Rollout auf Android/Fire TV (per Satellit + ADB) ─────────
|
// ── APK-Rollout auf Android/Fire TV (per Satellit + ADB) ─────────
|
||||||
let apkTarget = null; // {satId, ip, name}
|
let apkTarget = null; // {satId, ip, name}
|
||||||
let apkRequestId = null;
|
let apkRequestId = null;
|
||||||
@@ -4721,6 +4753,11 @@
|
|||||||
document.getElementById('apk-status').textContent = '';
|
document.getElementById('apk-status').textContent = '';
|
||||||
const log = document.getElementById('apk-log');
|
const log = document.getElementById('apk-log');
|
||||||
log.style.display = 'none'; log.textContent = '';
|
log.style.display = 'none'; log.textContent = '';
|
||||||
|
// Namensfeld vorbelegen — aber nicht mit Seriennummern/IP-Platzhaltern,
|
||||||
|
// die die Netz-Erkennung bei Fire TV oft liefert (z.B. 0d94400fa5b63301).
|
||||||
|
const looksTechnical = !name || /^[0-9a-f]{8,}$/i.test(name) ||
|
||||||
|
/^\d{1,3}(\.\d{1,3}){3}$/.test(name);
|
||||||
|
document.getElementById('apk-name').value = looksTechnical ? '' : name;
|
||||||
document.getElementById('apk-modal').classList.add('open');
|
document.getElementById('apk-modal').classList.add('open');
|
||||||
}
|
}
|
||||||
function closeApkDeploy() {
|
function closeApkDeploy() {
|
||||||
@@ -4732,9 +4769,53 @@
|
|||||||
const url = document.getElementById('apk-url').value.trim();
|
const url = document.getElementById('apk-url').value.trim();
|
||||||
if (!url) { document.getElementById('apk-status').textContent = 'APK-URL fehlt'; return; }
|
if (!url) { document.getElementById('apk-status').textContent = 'APK-URL fehlt'; return; }
|
||||||
apkRequestId = 'apk_' + Date.now() + '_' + Math.floor(Math.random() * 100000);
|
apkRequestId = 'apk_' + Date.now() + '_' + Math.floor(Math.random() * 100000);
|
||||||
document.getElementById('apk-status').textContent = 'läuft … (kann 1–2 Minuten dauern)';
|
document.getElementById('apk-status').innerHTML =
|
||||||
|
'<span style="color:#FFD60A;">läuft …</span>';
|
||||||
|
const log = document.getElementById('apk-log');
|
||||||
|
log.style.display = 'block';
|
||||||
|
log.innerHTML = '👀 <strong style="color:#FFD60A;">Jetzt auf den Bildschirm des Geräts schauen.</strong>\n' +
|
||||||
|
'Beim ersten Mal erscheint dort „USB-Debugging zulassen?" — bestätigen\n' +
|
||||||
|
'(Haken bei „Immer erlauben"). Ich warte bis zu 90 Sekunden darauf und\n' +
|
||||||
|
'installiere dann automatisch weiter. Kein zweiter Klick nötig.\n\n' +
|
||||||
|
'Danach: APK laden + installieren, zusammen 1–3 Minuten.';
|
||||||
|
const params = { ip: apkTarget.ip, url };
|
||||||
|
// Server ergaenzt Host/Port/Token — steht bewusst NICHT im Browser-Formular.
|
||||||
|
if (document.getElementById('apk-provision').checked) {
|
||||||
|
params.provision = true;
|
||||||
|
// Am PC getippter Name gewinnt; sonst der aus der Netz-Erkennung (der
|
||||||
|
// ist bei Fire TV oft nur die Seriennummer).
|
||||||
|
params.name = document.getElementById('apk-name').value.trim() || apkTarget.name;
|
||||||
|
}
|
||||||
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.install',
|
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.install',
|
||||||
requestId: apkRequestId, params: { ip: apkTarget.ip, url } });
|
requestId: apkRequestId, params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nur einrichten: RVS-Zugang + Namen setzen, ohne die APK neu zu installieren
|
||||||
|
* (z.B. um ein bereits ausgerolltes Gerät umzubenennen). */
|
||||||
|
function runApkProvision() {
|
||||||
|
if (!apkTarget) return;
|
||||||
|
apkRequestId = 'apk_' + Date.now() + '_' + Math.floor(Math.random() * 100000);
|
||||||
|
document.getElementById('apk-status').innerHTML = '<span style="color:#FFD60A;">richte ein …</span>';
|
||||||
|
const log = document.getElementById('apk-log');
|
||||||
|
log.style.display = 'block';
|
||||||
|
log.textContent = 'Übertrage Zugang und Namen an den Agenten …\n' +
|
||||||
|
'Die App startet dabei kurz auf dem Bildschirm — das ist normal.';
|
||||||
|
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.provision',
|
||||||
|
requestId: apkRequestId,
|
||||||
|
params: { ip: apkTarget.ip, provision: true,
|
||||||
|
name: document.getElementById('apk-name').value.trim() || apkTarget.name } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prüft, ob der Agent auf dem Gerät installiert ist und läuft. */
|
||||||
|
function runApkStatus() {
|
||||||
|
if (!apkTarget) return;
|
||||||
|
apkRequestId = 'apk_' + Date.now() + '_' + Math.floor(Math.random() * 100000);
|
||||||
|
document.getElementById('apk-status').innerHTML = '<span style="color:#FFD60A;">prüfe …</span>';
|
||||||
|
const log = document.getElementById('apk-log');
|
||||||
|
log.style.display = 'block';
|
||||||
|
log.textContent = 'Frage das Gerät über ADB ab …';
|
||||||
|
send({ action: 'sat_command', satellite: apkTarget.satId, cmd: 'adb.status',
|
||||||
|
requestId: apkRequestId, params: { ip: apkTarget.ip } });
|
||||||
}
|
}
|
||||||
function onSatResult(p) {
|
function onSatResult(p) {
|
||||||
if (!apkRequestId || !p || p.requestId !== apkRequestId) return;
|
if (!apkRequestId || !p || p.requestId !== apkRequestId) return;
|
||||||
@@ -4744,7 +4825,19 @@
|
|||||||
if (p.ok) {
|
if (p.ok) {
|
||||||
const r = p.result || {};
|
const r = p.result || {};
|
||||||
status.innerHTML = '<span style="color:#34C759;">✓ erfolgreich</span>';
|
status.innerHTML = '<span style="color:#34C759;">✓ erfolgreich</span>';
|
||||||
log.textContent = r.message || JSON.stringify(r, null, 2);
|
let txt = r.message || JSON.stringify(r, null, 2);
|
||||||
|
if (typeof r.installed !== 'undefined') { // adb.status
|
||||||
|
txt = (r.running ? '🟢 Agent läuft' : r.installed ? '🟡 installiert, läuft nicht' : '🔴 nicht installiert') +
|
||||||
|
(r.version ? '\nVersion: ' + r.version : '') +
|
||||||
|
'\nPaket: ' + (r.package || '') + '\nGerät: ' + (r.target || '');
|
||||||
|
if (!r.running && r.installed) {
|
||||||
|
txt += '\n\nTipp: Auf dem Gerät die App öffnen und „Speichern & Verbinden" —' +
|
||||||
|
'\noder hier „Gleich einrichten" nutzen, dann startet sie von selbst.';
|
||||||
|
}
|
||||||
|
} else if (r.provisioned === false) {
|
||||||
|
status.innerHTML = '<span style="color:#FF9500;">⚠ installiert, Einrichtung offen</span>';
|
||||||
|
}
|
||||||
|
log.textContent = txt;
|
||||||
} else {
|
} else {
|
||||||
status.innerHTML = '<span style="color:#FF6E6E;">✗ fehlgeschlagen</span>';
|
status.innerHTML = '<span style="color:#FF6E6E;">✗ fehlgeschlagen</span>';
|
||||||
log.textContent = p.error || 'Unbekannter Fehler';
|
log.textContent = p.error || 'Unbekannter Fehler';
|
||||||
@@ -4893,7 +4986,8 @@
|
|||||||
(scanning ? ' disabled' : '') + '>Geraete scannen</button>' +
|
(scanning ? ' disabled' : '') + '>Geraete scannen</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">kann: ' + escapeHtml(caps) + ' · ' + ctrl +
|
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">kann: ' + escapeHtml(caps) + ' · ' + ctrl +
|
||||||
(s.net && s.net.primary_ip ? ' · Netz: ' + escapeHtml(s.net.primary_ip) : '') + '</div>' +
|
(s.net && s.net.primary_ip ? ' · Netz: ' + escapeHtml(s.net.primary_ip) : '') +
|
||||||
|
(s.net && s.net.mac ? ' · MAC: <span style="font-family:monospace;">' + escapeHtml(s.net.mac) + '</span>' : '') + '</div>' +
|
||||||
(s.net && s.net.warning ? '<div style="color:#FF6E6E;font-size:11px;margin-top:6px;padding:6px;background:#3A1F1F;border-radius:4px;">⚠ ' + escapeHtml(s.net.warning) + '</div>' : '') +
|
(s.net && s.net.warning ? '<div style="color:#FF6E6E;font-size:11px;margin-top:6px;padding:6px;background:#3A1F1F;border-radius:4px;">⚠ ' + escapeHtml(s.net.warning) + '</div>' : '') +
|
||||||
devHtml +
|
devHtml +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|||||||
+20
-1
@@ -2812,6 +2812,14 @@ wss.on("connection", (ws) => {
|
|||||||
} else if (msg.action === "host_list") {
|
} else if (msg.action === "host_list") {
|
||||||
// Browser will die aktuelle Host-Agenten-Liste.
|
// Browser will die aktuelle Host-Agenten-Liste.
|
||||||
ws.send(JSON.stringify({ type: "host_update", hosts: hostList() }));
|
ws.send(JSON.stringify({ type: "host_update", hosts: hostList() }));
|
||||||
|
} else if (msg.action === "host_forget") {
|
||||||
|
// Alten/umbenannten Agenten aus der Liste werfen. Meldet er sich wieder
|
||||||
|
// (host_hello), taucht er automatisch neu auf — das ist also gefahrlos.
|
||||||
|
const id = String(msg.hostId || "");
|
||||||
|
if (id && hosts.delete(id)) {
|
||||||
|
log("info", "host", `Agent '${id}' aus der Liste entfernt`);
|
||||||
|
broadcastHosts();
|
||||||
|
}
|
||||||
} else if (msg.action === "rooms_query") {
|
} else if (msg.action === "rooms_query") {
|
||||||
// Browser will die RVS-Raum-Diagnose — via persistente rvsWs anfragen,
|
// Browser will die RVS-Raum-Diagnose — via persistente rvsWs anfragen,
|
||||||
// die Antwort (rooms_info) kommt auf derselben Verbindung zurueck.
|
// die Antwort (rooms_info) kommt auf derselben Verbindung zurueck.
|
||||||
@@ -2819,13 +2827,24 @@ wss.on("connection", (ws) => {
|
|||||||
} else if (msg.action === "sat_command") {
|
} else if (msg.action === "sat_command") {
|
||||||
// Browser loest eine Satelliten-Aktion aus (z.B. APK auf Fire TV ausrollen).
|
// Browser loest eine Satelliten-Aktion aus (z.B. APK auf Fire TV ausrollen).
|
||||||
// Die Antwort (sat_result) kommt ueber die rvsWs zurueck.
|
// Die Antwort (sat_result) kommt ueber die rvsWs zurueck.
|
||||||
|
const params = msg.params && typeof msg.params === "object" ? msg.params : {};
|
||||||
|
// Fern-Einrichtung: der RVS-Zugang wird SERVERSEITIG ergaenzt — so muss
|
||||||
|
// niemand Host/Token im Browser eintippen (und das Token steht nicht im
|
||||||
|
// Browser-Formular). Gilt fuer adb.provision und adb.install mit Setup.
|
||||||
|
if (params.provision === true) {
|
||||||
|
delete params.provision;
|
||||||
|
params.rvs_host = RVS_HOST;
|
||||||
|
params.rvs_port = Number(RVS_PORT) || 443;
|
||||||
|
params.rvs_tls = RVS_TLS === "true";
|
||||||
|
params.rvs_token = RVS_TOKEN;
|
||||||
|
}
|
||||||
sendToRVS_raw({
|
sendToRVS_raw({
|
||||||
type: "sat_command",
|
type: "sat_command",
|
||||||
payload: {
|
payload: {
|
||||||
satellite: String(msg.satellite || ""),
|
satellite: String(msg.satellite || ""),
|
||||||
device: String(msg.device || ""),
|
device: String(msg.device || ""),
|
||||||
action: String(msg.cmd || ""),
|
action: String(msg.cmd || ""),
|
||||||
params: msg.params && typeof msg.params === "object" ? msg.params : {},
|
params,
|
||||||
requestId: String(msg.requestId || ""),
|
requestId: String(msg.requestId || ""),
|
||||||
},
|
},
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
|||||||
@@ -87,6 +87,25 @@ Statt die APK von Hand aufs Gerät zu schaufeln, kann ein **Satellit** sie per A
|
|||||||
und einen **📦**-Button.
|
und einen **📦**-Button.
|
||||||
3. 📦 → APK-URL (aus dem Gitea-Release) → **Jetzt ausrollen**. Beim ersten Mal
|
3. 📦 → APK-URL (aus dem Gitea-Release) → **Jetzt ausrollen**. Beim ersten Mal
|
||||||
erscheint auf dem TV „USB-Debugging zulassen?" → bestätigen (Haken „Immer erlauben").
|
erscheint auf dem TV „USB-Debugging zulassen?" → bestätigen (Haken „Immer erlauben").
|
||||||
|
Der Rollout **wartet darauf** und macht dann automatisch weiter.
|
||||||
|
|
||||||
|
**Einrichtung ohne Tippen:** Am Fire TV gibt es keine Kamera (kein QR-Scan) und
|
||||||
|
Host/Token mit der Fernbedienung einzugeben ist eine Qual. Deshalb ist im Rollout-
|
||||||
|
Dialog **„Gleich einrichten"** vorausgewählt: die Diagnostic kennt den RVS-Zugang
|
||||||
|
selbst und gibt ihn dem Agenten über dieselbe ADB-Verbindung mit
|
||||||
|
(`adb.provision` → `am start` mit Intent-Extras). Der Agent speichert die Daten,
|
||||||
|
startet den Dienst und ist **sofort online** — null Eingaben am Fernseher.
|
||||||
|
Das Token geht dabei **serverseitig** raus, es steht nie im Browser-Formular.
|
||||||
|
|
||||||
|
Mit **„Status prüfen"** (`adb.status`) siehst Du jederzeit, ob der Agent auf dem
|
||||||
|
Gerät installiert ist, welche Version läuft und ob der Dienst aktiv ist.
|
||||||
|
|
||||||
|
> **Sicherheitshinweis:** Die Einrichtung per Intent geht an die (exportierte)
|
||||||
|
> Haupt-Activity. Wer ADB-Zugriff auf das Gerät hat, kann den Agenten damit
|
||||||
|
> umkonfigurieren — wer ADB hat, kontrolliert das Gerät ohnehin. Die App zeigt
|
||||||
|
> beim Empfang einen Hinweis („Von ARIA eingerichtet"), damit es nicht unbemerkt
|
||||||
|
> passiert. Auf einem dedizierten Stick ist das unkritisch; schalte ADB-Debugging
|
||||||
|
> nach dem Ausrollen ab, wenn Du ganz sicher gehen willst.
|
||||||
|
|
||||||
ARIA kann dasselbe: `satellite_command` mit `action='adb.install'`,
|
ARIA kann dasselbe: `satellite_command` mit `action='adb.install'`,
|
||||||
`params={'ip':'…','url':'…'}`. Der Satellit lädt die APK **einmal** und kann sie
|
`params={'ip':'…','url':'…'}`. Der Satellit lädt die APK **einmal** und kann sie
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId 'de.hackersoft.ariaagent'
|
applicationId 'de.hackersoft.ariaagent'
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
|
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
|
||||||
versionCode 8
|
versionCode 101
|
||||||
versionName '0.0.0.8'
|
versionName '0.0.1.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
|
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -122,7 +122,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadIntoUi(AgentConfig.load(this))
|
// Fern-Einrichtung (z.B. Fire TV, wo QR-Scan und Tippen wegfallen): der
|
||||||
|
// Satellit startet die App per ADB mit den Zugangsdaten als Extras.
|
||||||
|
// Hat das geklappt, ist die UI schon befuellt — sonst normal laden.
|
||||||
|
if (!applyProvisioning(intent)) loadIntoUi(AgentConfig.load(this))
|
||||||
|
|
||||||
// Fire TV: kein Touchscreen — der QR-Scan ist dort sinnlos (keine Kamera),
|
// Fire TV: kein Touchscreen — der QR-Scan ist dort sinnlos (keine Kamera),
|
||||||
// und der erste Fokus muss gesetzt sein, damit die Fernbedienung greift.
|
// und der erste Fokus muss gesetzt sein, damit die Fernbedienung greift.
|
||||||
@@ -209,6 +212,50 @@ class MainActivity : AppCompatActivity() {
|
|||||||
statusView.text = "Status: ${AgentService.status} $dot"
|
statusView.text = "Status: ${AgentService.status} $dot"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
if (intent == null) return
|
||||||
|
setIntent(intent)
|
||||||
|
applyProvisioning(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fern-Einrichtung per Intent-Extras (vom Satelliten via ADB gestartet).
|
||||||
|
*
|
||||||
|
* Auf einem Fire TV gibt es keine Kamera (kein QR) und Tippen mit der
|
||||||
|
* Fernbedienung ist muehsam — deshalb kann der Satellit die Zugangsdaten
|
||||||
|
* direkt mitgeben:
|
||||||
|
* adb shell am start -n de.hackersoft.ariaagent/.MainActivity \
|
||||||
|
* -a de.hackersoft.ariaagent.PROVISION \
|
||||||
|
* -e host rvs.example.com --ei port 443 --ez tls true -e token XYZ
|
||||||
|
*
|
||||||
|
* Gibt true zurueck, wenn eingerichtet wurde (dann startet der Dienst gleich).
|
||||||
|
*/
|
||||||
|
private fun applyProvisioning(intent: Intent): Boolean {
|
||||||
|
val host = intent.getStringExtra("host")?.trim().orEmpty()
|
||||||
|
val token = intent.getStringExtra("token")?.trim().orEmpty()
|
||||||
|
if (host.isEmpty() || token.isEmpty()) return false
|
||||||
|
|
||||||
|
val old = AgentConfig.load(this)
|
||||||
|
val cfg = AgentConfig(
|
||||||
|
host = host,
|
||||||
|
port = intent.getIntExtra("port", old.port.takeIf { it > 0 } ?: 443),
|
||||||
|
tls = intent.getBooleanExtra("tls", old.tls),
|
||||||
|
token = token,
|
||||||
|
name = intent.getStringExtra("name")?.trim().takeUnless { it.isNullOrEmpty() } ?: old.name,
|
||||||
|
controlEnabled = intent.getBooleanExtra("control", true),
|
||||||
|
replyMode = intent.getStringExtra("reply_mode")?.trim()
|
||||||
|
.takeUnless { it.isNullOrEmpty() } ?: old.replyMode,
|
||||||
|
)
|
||||||
|
cfg.save(this)
|
||||||
|
AgentService.start(this)
|
||||||
|
// Sichtbar machen, dass die Konfiguration von aussen kam (Transparenz).
|
||||||
|
toast("Von ARIA eingerichtet: ${cfg.host} — Agent gestartet")
|
||||||
|
loadIntoUi(cfg)
|
||||||
|
refreshStatus()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/** Sprech-Taste: startet/stoppt die Aufnahme im laufenden Dienst. */
|
/** Sprech-Taste: startet/stoppt die Aufnahme im laufenden Dienst. */
|
||||||
private fun talkToAria() {
|
private fun talkToAria() {
|
||||||
if (!AgentConfig.load(this).isValid()) { toast("Erst verbinden"); return }
|
if (!AgentConfig.load(this).isValid()) { toast("Erst verbinden"); return }
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ OUT_MAX_CHARS_HARD = int(os.environ.get("OUT_MAX_CHARS_HARD", "200000") or "2000
|
|||||||
FILE_MAX_BYTES = int(os.environ.get("FILE_MAX_BYTES", str(10 * 1024 * 1024)) or str(10 * 1024 * 1024))
|
FILE_MAX_BYTES = int(os.environ.get("FILE_MAX_BYTES", str(10 * 1024 * 1024)) or str(10 * 1024 * 1024))
|
||||||
|
|
||||||
# Version (wird von release_agent.sh beim Release gesetzt).
|
# Version (wird von release_agent.sh beim Release gesetzt).
|
||||||
AGENT_VERSION = "0.0.0.8"
|
AGENT_VERSION = "0.0.1.1"
|
||||||
|
|
||||||
HEARTBEAT_SEC = 25
|
HEARTBEAT_SEC = 25
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,12 @@ CONTROL_ENABLED=true
|
|||||||
# adb.connect ADB-Verbindung aufbauen (params: ip)
|
# adb.connect ADB-Verbindung aufbauen (params: ip)
|
||||||
# adb.devices verbundene ADB-Geraete auflisten
|
# adb.devices verbundene ADB-Geraete auflisten
|
||||||
# adb.shell Shell-Kommando auf dem Android-Geraet (params: ip, cmd)
|
# adb.shell Shell-Kommando auf dem Android-Geraet (params: ip, cmd)
|
||||||
|
# adb.provision Agent FERN einrichten: RVS-Zugang uebergeben, statt ihn mit der
|
||||||
|
# Fernbedienung abzutippen (params: ip, rvs_host, rvs_token, ...)
|
||||||
|
# adb.status laeuft/installiert der Agent auf dem Geraet? (params: ip)
|
||||||
# ACHTUNG: Diese Liste ueberschreibt den Code-Default. Wird eine neue Aktion
|
# ACHTUNG: Diese Liste ueberschreibt den Code-Default. Wird eine neue Aktion
|
||||||
# ergaenzt, MUSS sie hier nachgetragen werden — sonst lehnt der Satellit sie ab.
|
# ergaenzt, MUSS sie hier nachgetragen werden — sonst lehnt der Satellit sie ab.
|
||||||
CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,adb.devices,adb.connect,adb.install,adb.shell
|
CONTROL_ALLOWLIST=dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,adb.devices,adb.connect,adb.install,adb.shell,adb.provision,adb.status
|
||||||
|
|
||||||
# ─── ADB / Android-Rollout (optional) ──────────────────────────────
|
# ─── ADB / Android-Rollout (optional) ──────────────────────────────
|
||||||
# Fire TV Sticks & Android-Geraete im LAN: APK ausrollen ohne Datei-Kopieren.
|
# Fire TV Sticks & Android-Geraete im LAN: APK ausrollen ohne Datei-Kopieren.
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ docker compose logs -f # "Netz: primary_ip=192.168.0.x" + "[scan] N Ger
|
|||||||
```
|
```
|
||||||
`network_mode: host` (schon gesetzt) gibt hier echtes LAN + Multicast.
|
`network_mode: host` (schon gesetzt) gibt hier echtes LAN + Multicast.
|
||||||
|
|
||||||
|
> **Fehler `KeyError: 'ContainerConfig'` beim Neubauen?**
|
||||||
|
> Das ist ein **Bug im alten `docker-compose` 1.29.2** (Python-Version), nicht im
|
||||||
|
> Satelliten: Beim *Ersetzen* eines bestehenden Containers mit Volume liest es ein
|
||||||
|
> Feld aus dem Image, das neuere Docker-Versionen nicht mehr schreiben. Zwei Fixes:
|
||||||
|
> ```bash
|
||||||
|
> docker compose up -d --build # v2 (mit LEERZEICHEN) — falls vorhanden
|
||||||
|
> # oder mit der alten Version: alten Container erst weg, dann neu anlegen
|
||||||
|
> docker-compose rm -sf satellite && docker-compose up -d --build
|
||||||
|
> ```
|
||||||
|
> Der `./data`-Ordner (Credentials, ADB-Schlüssel, APK-Cache) bleibt dabei erhalten —
|
||||||
|
> er ist ein Bind-Mount, kein Container-Volume.
|
||||||
|
|
||||||
**B) Nativ als Python-Prozess** (für Mac/Windows-Test oder ohne Docker) — läuft direkt
|
**B) Nativ als Python-Prozess** (für Mac/Windows-Test oder ohne Docker) — läuft direkt
|
||||||
auf einer Maschine im Ziel-LAN. Das Script lädt die `.env` **selbst** (muss im
|
auf einer Maschine im Ziel-LAN. Das Script lädt die `.env` **selbst** (muss im
|
||||||
`satellite/`-Ordner liegen):
|
`satellite/`-Ordner liegen):
|
||||||
|
|||||||
+238
-12
@@ -135,7 +135,7 @@ CONTROL_ALLOWLIST = [
|
|||||||
os.environ.get("CONTROL_ALLOWLIST",
|
os.environ.get("CONTROL_ALLOWLIST",
|
||||||
"dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,"
|
"dial.launch,wol,http.get,snmp.get,snmp.walk,snmp.printer,"
|
||||||
"snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,"
|
"snmp.ports,snmp.info,fritzbox.info,fritzbox.hosts,ssh.exec,"
|
||||||
"adb.devices,adb.connect,adb.install,adb.shell").split(",")
|
"adb.devices,adb.connect,adb.install,adb.shell,adb.provision,adb.status").split(",")
|
||||||
if a.strip()
|
if a.strip()
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -145,6 +145,10 @@ CONTROL_ALLOWLIST = [
|
|||||||
# Download brauchen.
|
# Download brauchen.
|
||||||
ADB_PORT = int(os.environ.get("ADB_PORT", "5555") or "5555")
|
ADB_PORT = int(os.environ.get("ADB_PORT", "5555") or "5555")
|
||||||
ADB_TIMEOUT_SEC = float(os.environ.get("ADB_TIMEOUT_SEC", "180") or "180")
|
ADB_TIMEOUT_SEC = float(os.environ.get("ADB_TIMEOUT_SEC", "180") or "180")
|
||||||
|
# Beim ERSTEN Verbinden zeigt das Geraet "USB-Debugging zulassen?" auf dem
|
||||||
|
# Bildschirm. Statt mit Fehler abzubrechen (und den Nutzer nochmal klicken zu
|
||||||
|
# lassen) wartet der Satellit so lange auf die Bestaetigung.
|
||||||
|
ADB_AUTH_WAIT_SEC = float(os.environ.get("ADB_AUTH_WAIT_SEC", "90") or "90")
|
||||||
APK_CACHE_DIR = os.environ.get("APK_CACHE_DIR", "/data/apk")
|
APK_CACHE_DIR = os.environ.get("APK_CACHE_DIR", "/data/apk")
|
||||||
APK_MAX_BYTES = int(os.environ.get("APK_MAX_BYTES", str(64 * 1024 * 1024)) or str(64 * 1024 * 1024))
|
APK_MAX_BYTES = int(os.environ.get("APK_MAX_BYTES", str(64 * 1024 * 1024)) or str(64 * 1024 * 1024))
|
||||||
|
|
||||||
@@ -329,7 +333,48 @@ def _net_context() -> dict:
|
|||||||
"mDNS/SSDP erreichen die realen Geraete so nicht. Auf Docker Desktop "
|
"mDNS/SSDP erreichen die realen Geraete so nicht. Auf Docker Desktop "
|
||||||
"(Mac/Windows) geht LAN-Discovery nicht — den Satelliten NATIV (python "
|
"(Mac/Windows) geht LAN-Discovery nicht — den Satelliten NATIV (python "
|
||||||
"satellite.py) oder auf einem Linux-Host im Ziel-LAN betreiben.")
|
"satellite.py) oder auf einem Linux-Host im Ziel-LAN betreiben.")
|
||||||
return {"primary_ip": p, "ips": ips, "warning": warning}
|
return {"primary_ip": p, "ips": ips, "mac": _own_mac(p), "warning": warning}
|
||||||
|
|
||||||
|
|
||||||
|
def _own_mac(ip: str) -> str:
|
||||||
|
"""MAC der eigenen (primaeren) Netzwerkkarte — identifiziert die Kiste
|
||||||
|
eindeutig (DHCP-Reservierung, WOL). Liest /sys/class/net (Linux); faellt auf
|
||||||
|
uuid.getnode() zurueck, wenn das Interface nicht zuzuordnen ist."""
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
base = "/sys/class/net"
|
||||||
|
if os.path.isdir(base):
|
||||||
|
# Interface zur primaeren IP finden (ueber die Adressen im /proc-Netz).
|
||||||
|
want = ""
|
||||||
|
if ip:
|
||||||
|
try:
|
||||||
|
out = subprocess.run(["ip", "-o", "-4", "addr", "show"],
|
||||||
|
capture_output=True, text=True, timeout=3).stdout
|
||||||
|
for line in out.splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 4 and parts[3].split("/")[0] == ip:
|
||||||
|
want = parts[1]
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
names = [want] if want else []
|
||||||
|
names += [n for n in sorted(os.listdir(base)) if n != "lo" and n not in names]
|
||||||
|
for n in names:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(base, n, "address"), "r") as f:
|
||||||
|
mac = f.read().strip()
|
||||||
|
if mac and mac != "00:00:00:00:00:00":
|
||||||
|
return mac.lower()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
import uuid
|
||||||
|
node = uuid.getnode()
|
||||||
|
return ":".join(f"{(node >> e) & 0xFF:02x}" for e in range(40, -8, -8))
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
NET = _net_context()
|
NET = _net_context()
|
||||||
@@ -588,6 +633,10 @@ async def _control(action: str, params: dict, devices: list[dict]) -> dict:
|
|||||||
return await loop.run_in_executor(None, _do_adb_install, params)
|
return await loop.run_in_executor(None, _do_adb_install, params)
|
||||||
if action == "adb.shell":
|
if action == "adb.shell":
|
||||||
return await loop.run_in_executor(None, _do_adb_shell, params)
|
return await loop.run_in_executor(None, _do_adb_shell, params)
|
||||||
|
if action == "adb.provision":
|
||||||
|
return await loop.run_in_executor(None, _do_adb_provision, params)
|
||||||
|
if action == "adb.status":
|
||||||
|
return await loop.run_in_executor(None, _do_adb_status, params)
|
||||||
return {"ok": False, "error": f"Aktion '{action}' nicht implementiert."}
|
return {"ok": False, "error": f"Aktion '{action}' nicht implementiert."}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"ok": False, "error": f"{action} fehlgeschlagen: {exc}"}
|
return {"ok": False, "error": f"{action} fehlgeschlagen: {exc}"}
|
||||||
@@ -1121,6 +1170,28 @@ def _adb_state(target: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _adb_wait_authorized(target: str, seconds: float) -> tuple:
|
||||||
|
"""Wartet, bis das Geraet autorisiert ist ('device').
|
||||||
|
|
||||||
|
Beim ersten Kontakt erscheint auf dem Fernseher/Handy der Dialog
|
||||||
|
"USB-Debugging zulassen?". Frueher brach die Installation hier ab und man
|
||||||
|
musste nach dem Bestaetigen ein zweites Mal starten — jetzt wartet der
|
||||||
|
Satellit einfach, bis der Nutzer bestaetigt hat. Gibt (ok, zustand).
|
||||||
|
"""
|
||||||
|
import time as _time
|
||||||
|
deadline = _time.time() + max(0.0, seconds)
|
||||||
|
last = _adb_state(target)
|
||||||
|
while _time.time() < deadline:
|
||||||
|
if last == "device":
|
||||||
|
return True, last
|
||||||
|
_time.sleep(2.0)
|
||||||
|
# Erneut verbinden: nach dem Bestaetigen wechselt der Zustand oft erst
|
||||||
|
# mit einem frischen connect von 'unauthorized'/'offline' auf 'device'.
|
||||||
|
_adb_run(["connect", target], timeout=15)
|
||||||
|
last = _adb_state(target)
|
||||||
|
return last == "device", last
|
||||||
|
|
||||||
|
|
||||||
def _do_adb_devices(params: dict) -> dict:
|
def _do_adb_devices(params: dict) -> dict:
|
||||||
"""Listet die dem Satelliten bekannten ADB-Geraete."""
|
"""Listet die dem Satelliten bekannten ADB-Geraete."""
|
||||||
rc, out, err = _adb_run(["devices", "-l"], timeout=20)
|
rc, out, err = _adb_run(["devices", "-l"], timeout=20)
|
||||||
@@ -1140,10 +1211,142 @@ def _do_adb_connect(params: dict) -> dict:
|
|||||||
if not target:
|
if not target:
|
||||||
return {"ok": False, "error": "ip/host erforderlich."}
|
return {"ok": False, "error": "ip/host erforderlich."}
|
||||||
ok, msg = _adb_connect(target)
|
ok, msg = _adb_connect(target)
|
||||||
|
state = _adb_state(target)
|
||||||
|
wait = float(params.get("auth_wait") or ADB_AUTH_WAIT_SEC)
|
||||||
|
if (not ok or state != "device") and wait > 0:
|
||||||
|
authed, state = _adb_wait_authorized(target, wait)
|
||||||
|
if not authed:
|
||||||
|
return {"ok": False, "error":
|
||||||
|
"Nicht autorisiert — auf dem Bildschirm des Geraets "
|
||||||
|
"'USB-Debugging zulassen?' bestaetigen. "
|
||||||
|
f"(gewartet: {wait:.0f}s, Zustand: '{state or '?'}')"}
|
||||||
|
msg = f"verbunden (nach Bestaetigung am Geraet)"
|
||||||
|
elif not ok:
|
||||||
|
return {"ok": False, "error": msg}
|
||||||
|
return {"ok": True, "result": {"target": target, "state": state, "message": msg}}
|
||||||
|
|
||||||
|
|
||||||
|
AGENT_PACKAGE = os.environ.get("AGENT_PACKAGE", "de.hackersoft.ariaagent")
|
||||||
|
|
||||||
|
|
||||||
|
def _adb_app_installed(target: str, package: str) -> bool:
|
||||||
|
rc, out, _ = _adb_run(["-s", target, "shell", "pm", "list", "packages", package],
|
||||||
|
timeout=30)
|
||||||
|
return rc == 0 and f"package:{package}" in out
|
||||||
|
|
||||||
|
|
||||||
|
def _adb_app_running(target: str, package: str) -> bool:
|
||||||
|
"""Laeuft der Vordergrund-Dienst des Agents? (Fire TV / Android)."""
|
||||||
|
rc, out, _ = _adb_run(["-s", target, "shell", "dumpsys", "activity", "services",
|
||||||
|
package], timeout=30)
|
||||||
|
return rc == 0 and "ServiceRecord" in out
|
||||||
|
|
||||||
|
|
||||||
|
def _do_adb_status(params: dict) -> dict:
|
||||||
|
"""Ist der ARIA-Agent auf dem Geraet installiert und laeuft er?"""
|
||||||
|
target = _adb_target(params)
|
||||||
|
if not target:
|
||||||
|
return {"ok": False, "error": "ip/host erforderlich."}
|
||||||
|
package = (params.get("package") or AGENT_PACKAGE).strip()
|
||||||
|
ok, msg = _adb_connect(target)
|
||||||
if not ok:
|
if not ok:
|
||||||
return {"ok": False, "error": msg}
|
return {"ok": False, "error": msg}
|
||||||
return {"ok": True, "result": {"target": target, "state": _adb_state(target),
|
installed = _adb_app_installed(target, package)
|
||||||
"message": msg}}
|
running = _adb_app_running(target, package) if installed else False
|
||||||
|
ver = ""
|
||||||
|
if installed:
|
||||||
|
rc, out, _ = _adb_run(["-s", target, "shell", "dumpsys", "package", package],
|
||||||
|
timeout=30)
|
||||||
|
m = re.search(r"versionName=(\S+)", out or "")
|
||||||
|
ver = m.group(1) if m else ""
|
||||||
|
return {"ok": True, "result": {
|
||||||
|
"target": target, "package": package, "installed": installed,
|
||||||
|
"running": running, "version": ver,
|
||||||
|
"message": ("Agent laeuft" if running else
|
||||||
|
"installiert, laeuft aber nicht" if installed else
|
||||||
|
"nicht installiert")}}
|
||||||
|
|
||||||
|
|
||||||
|
def _do_adb_provision(params: dict) -> dict:
|
||||||
|
"""Richtet den ARIA-Agent auf dem Geraet FERN ein (RVS-Zugang uebergeben).
|
||||||
|
|
||||||
|
Auf einem Fire TV gibt es keine Kamera (kein QR-Scan) und Tippen mit der
|
||||||
|
Fernbedienung ist muehsam. Der Satellit startet die App deshalb per ADB
|
||||||
|
direkt mit den Zugangsdaten als Intent-Extras.
|
||||||
|
params: {ip, rvs_host, rvs_token, rvs_port?, rvs_tls?, name?, control?,
|
||||||
|
reply_mode?, package?}
|
||||||
|
"""
|
||||||
|
target = _adb_target(params)
|
||||||
|
if not target:
|
||||||
|
return {"ok": False, "error": "ip/host erforderlich."}
|
||||||
|
rvs_host = (params.get("rvs_host") or "").strip()
|
||||||
|
rvs_token = (params.get("rvs_token") or "").strip()
|
||||||
|
if not rvs_host or not rvs_token:
|
||||||
|
return {"ok": False, "error": "rvs_host und rvs_token erforderlich."}
|
||||||
|
package = (params.get("package") or AGENT_PACKAGE).strip()
|
||||||
|
|
||||||
|
ok, msg = _adb_connect(target)
|
||||||
|
state = _adb_state(target)
|
||||||
|
wait = float(params.get("auth_wait") or ADB_AUTH_WAIT_SEC)
|
||||||
|
if (not ok or state != "device") and wait > 0:
|
||||||
|
authed, state = _adb_wait_authorized(target, wait)
|
||||||
|
if not authed:
|
||||||
|
return {"ok": False, "error":
|
||||||
|
"Nicht autorisiert — 'USB-Debugging zulassen?' auf dem "
|
||||||
|
f"Bildschirm bestaetigen. (Zustand: '{state or '?'}')"}
|
||||||
|
elif not ok:
|
||||||
|
return {"ok": False, "error": msg}
|
||||||
|
|
||||||
|
if not _adb_app_installed(target, package):
|
||||||
|
return {"ok": False, "error": f"'{package}' ist nicht installiert — "
|
||||||
|
"erst adb.install ausfuehren."}
|
||||||
|
|
||||||
|
# WICHTIG: 'adb shell' haengt die Argumente zu EINER Kommandozeile zusammen,
|
||||||
|
# die auf dem Geraet nochmal von der Shell zerlegt wird. Werte mit Leerzeichen
|
||||||
|
# ("Wohnzimmer Fire TV") wuerden dabei abgeschnitten -> jeden Wert quoten.
|
||||||
|
import shlex
|
||||||
|
q = shlex.quote
|
||||||
|
# Der Intent MUSS eindeutig sein: Android vergleicht beim Start Komponente,
|
||||||
|
# Action und Daten — NICHT die Extras. Ohne eigene Daten-URI gilt ein zweites
|
||||||
|
# Provisioning als "gleicher Intent", die Activity wird nur nach vorn geholt
|
||||||
|
# und onCreate/onNewIntent laufen nie -> der alte Name bliebe stehen.
|
||||||
|
stamp = str(int(time.time() * 1000))
|
||||||
|
args = ["-s", target, "shell", "am", "start",
|
||||||
|
"-n", f"{package}/.MainActivity",
|
||||||
|
"-a", f"{package}.PROVISION",
|
||||||
|
"-d", q(f"aria://provision/{stamp}"),
|
||||||
|
"-e", "host", q(rvs_host),
|
||||||
|
"-e", "token", q(rvs_token),
|
||||||
|
"--ei", "port", str(int(params.get("rvs_port") or 443)),
|
||||||
|
"--ez", "tls", "true" if _as_bool(params.get("rvs_tls"), True) else "false",
|
||||||
|
"--ez", "control", "true" if _as_bool(params.get("control"), True) else "false"]
|
||||||
|
if params.get("name"):
|
||||||
|
args += ["-e", "name", q(str(params["name"]))]
|
||||||
|
if params.get("reply_mode"):
|
||||||
|
args += ["-e", "reply_mode", q(str(params["reply_mode"]))]
|
||||||
|
|
||||||
|
rc, out, err = _adb_run(args, timeout=60)
|
||||||
|
txt = f"{out} {err}".strip()
|
||||||
|
if rc != 0 or "Error" in txt:
|
||||||
|
return {"ok": False, "error": (txt or f"exit {rc}")[:400]}
|
||||||
|
# Android meldet das, wenn es den Intent als "schon offen" abtut und die
|
||||||
|
# Activity nur nach vorn holt — dann kaeme die neue Konfiguration NICHT an.
|
||||||
|
if "brought to the front" in txt.lower() or "not started" in txt.lower():
|
||||||
|
return {"ok": False, "error":
|
||||||
|
"Android hat den Einrichtungs-Intent nicht ausgeliefert (Activity "
|
||||||
|
"war schon offen). Auf dem Geraet die Agent-App einmal schliessen "
|
||||||
|
"und erneut 'Nur einrichten' druecken. Details: " + txt[:200]}
|
||||||
|
return {"ok": True, "result": {
|
||||||
|
"target": target, "package": package,
|
||||||
|
"message": f"Agent eingerichtet (RVS {rvs_host}) und gestartet"}}
|
||||||
|
|
||||||
|
|
||||||
|
def _as_bool(v, default: bool) -> bool:
|
||||||
|
if v is None:
|
||||||
|
return default
|
||||||
|
if isinstance(v, bool):
|
||||||
|
return v
|
||||||
|
return str(v).strip().lower() in ("1", "true", "yes", "on", "ja")
|
||||||
|
|
||||||
|
|
||||||
def _do_adb_shell(params: dict) -> dict:
|
def _do_adb_shell(params: dict) -> dict:
|
||||||
@@ -1241,12 +1444,23 @@ def _do_adb_install(params: dict) -> dict:
|
|||||||
return {"ok": False, "error": err}
|
return {"ok": False, "error": err}
|
||||||
|
|
||||||
ok, msg = _adb_connect(target)
|
ok, msg = _adb_connect(target)
|
||||||
if not ok:
|
|
||||||
return {"ok": False, "error": msg}
|
|
||||||
state = _adb_state(target)
|
state = _adb_state(target)
|
||||||
if state and state != "device":
|
# Erstkontakt: Das Geraet zeigt jetzt "USB-Debugging zulassen?" auf dem
|
||||||
return {"ok": False, "error": f"Geraet im Zustand '{state}' — "
|
# Bildschirm. Statt abzubrechen warten wir auf die Bestaetigung — ein Klick
|
||||||
"bei 'unauthorized' den Dialog auf dem Bildschirm bestaetigen."}
|
# im Diagnostic genuegt, kein zweiter Anlauf noetig.
|
||||||
|
wait = float(params.get("auth_wait") or ADB_AUTH_WAIT_SEC)
|
||||||
|
if (not ok or state != "device") and wait > 0:
|
||||||
|
logger.info("[adb] %s: warte bis zu %.0fs auf Autorisierung (Zustand '%s')",
|
||||||
|
target, wait, state or "?")
|
||||||
|
authed, state = _adb_wait_authorized(target, wait)
|
||||||
|
if not authed:
|
||||||
|
hint = ("Das Geraet hat die Verbindung nicht autorisiert. Auf dem "
|
||||||
|
"Bildschirm erscheint 'USB-Debugging zulassen?' — bitte "
|
||||||
|
"bestaetigen (Haken bei 'Immer erlauben'). "
|
||||||
|
f"Ich habe {wait:.0f}s gewartet, Zustand: '{state or 'nicht erreichbar'}'.")
|
||||||
|
return {"ok": False, "error": hint if (state or ok) else msg}
|
||||||
|
elif not ok:
|
||||||
|
return {"ok": False, "error": msg}
|
||||||
|
|
||||||
args = ["-s", target, "install", "-r"] # -r = Update statt Neuinstallation
|
args = ["-s", target, "install", "-r"] # -r = Update statt Neuinstallation
|
||||||
if params.get("downgrade"):
|
if params.get("downgrade"):
|
||||||
@@ -1256,8 +1470,19 @@ def _do_adb_install(params: dict) -> dict:
|
|||||||
txt = f"{out} {err2}".strip()
|
txt = f"{out} {err2}".strip()
|
||||||
size_mb = round(os.path.getsize(apk) / 1024 / 1024, 1)
|
size_mb = round(os.path.getsize(apk) / 1024 / 1024, 1)
|
||||||
if rc == 0 and "success" in txt.lower():
|
if rc == 0 and "success" in txt.lower():
|
||||||
return {"ok": True, "result": {"target": target, "apk_mb": size_mb,
|
res = {"target": target, "apk_mb": size_mb,
|
||||||
"message": f"APK installiert ({size_mb} MB)"}}
|
"message": f"APK installiert ({size_mb} MB)"}
|
||||||
|
# Direkt einrichten, wenn die RVS-Daten mitkamen — auf einem Fire TV
|
||||||
|
# spart das der Fernbedienung das Abtippen von Host und Token.
|
||||||
|
if params.get("rvs_host") and params.get("rvs_token"):
|
||||||
|
prov = _do_adb_provision(params)
|
||||||
|
res["provisioned"] = bool(prov.get("ok"))
|
||||||
|
if prov.get("ok"):
|
||||||
|
res["message"] += " und eingerichtet — der Agent sollte gleich online sein"
|
||||||
|
else:
|
||||||
|
res["message"] += (f". Einrichtung fehlgeschlagen: "
|
||||||
|
f"{prov.get('error', '?')}")
|
||||||
|
return {"ok": True, "result": res}
|
||||||
low = txt.lower()
|
low = txt.lower()
|
||||||
if "signatures do not match" in low or "update_incompatible" in low:
|
if "signatures do not match" in low or "update_incompatible" in low:
|
||||||
return {"ok": False, "error": "Signatur passt nicht zur installierten Version — "
|
return {"ok": False, "error": "Signatur passt nicht zur installierten Version — "
|
||||||
@@ -1640,7 +1865,8 @@ class Satellite:
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
logger.info("ARIA Satellit startet — id=%s location=%s control=%s",
|
logger.info("ARIA Satellit startet — id=%s location=%s control=%s",
|
||||||
SATELLITE_ID, SATELLITE_LOCATION, CONTROL_ENABLED)
|
SATELLITE_ID, SATELLITE_LOCATION, CONTROL_ENABLED)
|
||||||
logger.info("Netz: primary_ip=%s alle=%s", NET.get("primary_ip"), NET.get("ips"))
|
logger.info("Netz: primary_ip=%s mac=%s alle=%s",
|
||||||
|
NET.get("primary_ip"), NET.get("mac") or "?", NET.get("ips"))
|
||||||
if NET.get("warning"):
|
if NET.get("warning"):
|
||||||
logger.warning("⚠ %s", NET["warning"])
|
logger.warning("⚠ %s", NET["warning"])
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user