Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b84ee3b9c | ||
|
|
c86cd59b5c | ||
|
|
96012dc986 | ||
|
|
80d7f62eaa | ||
|
|
5f5234ee62 | ||
|
|
28e58089aa | ||
|
|
c1a90102f8 | ||
|
|
ba531adc74 | ||
|
|
04d29b256e | ||
|
|
e6e07f672a | ||
|
|
d32e7e59c3 | ||
|
|
4f8e65b76b | ||
|
|
a2605a2802 | ||
|
|
0588a8d9b3 |
@@ -4,6 +4,12 @@
|
||||
FROM python:3.11-slim-bullseye
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# PyInstaller braucht objdump aus binutils (im slim-Image nicht enthalten).
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends binutils \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir pyinstaller
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Baut die Windows-.exe des Host-Agents AUF LINUX — via Wine + Windows-Python +
|
||||
# PyInstaller. tobix/pywine bringt Wine + Windows-Python 3.11 mit (PyInstaller
|
||||
# kann NICHT cross-compilen, deshalb der Wine-Umweg).
|
||||
#
|
||||
# Zusaetzlich baut NSIS ein setup.exe, das die Agent-.exe installiert, eine .env
|
||||
# in C:\ProgramData\ARIA-Host-Agent anlegt (falls keine da ist) und den Agent als
|
||||
# automatisch startenden Windows-Dienst (via nssm) einrichtet.
|
||||
FROM tobix/pywine:3.11
|
||||
|
||||
ARG VERSION=0.0.0
|
||||
|
||||
# NSIS (Installer-Compiler, laeuft nativ auf Linux) + Tools fuer nssm.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nsis curl unzip ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
# nssm — Non-Sucking Service Manager (public domain): macht aus der Konsolen-.exe
|
||||
# einen sauberen Windows-Dienst (die .exe selbst spricht das SCM nicht).
|
||||
RUN curl -fsSL https://nssm.cc/release/nssm-2.24.zip -o /tmp/nssm.zip \
|
||||
&& unzip -q /tmp/nssm.zip -d /tmp \
|
||||
&& cp /tmp/nssm-2.24/win64/nssm.exe ./nssm.exe \
|
||||
&& rm -rf /tmp/nssm*
|
||||
|
||||
COPY requirements.txt host_agent.py ./
|
||||
COPY windows/installer.nsi ./
|
||||
|
||||
# Windows-Python-Deps + PyInstaller, dann die Onefile-.exe bauen.
|
||||
RUN wine pip install --no-cache-dir -r requirements.txt pyinstaller
|
||||
RUN wine pyinstaller --onefile --name aria-host-agent --collect-all psutil host_agent.py \
|
||||
&& cp dist/aria-host-agent.exe ./aria-host-agent.exe \
|
||||
&& makensis -DVERSION=${VERSION} installer.nsi
|
||||
|
||||
# Beide Artefakte rausreichen (dist/ wird vom build-win.sh als Volume gemountet).
|
||||
CMD ["bash","-lc","cp dist/aria-host-agent.exe /out/ && cp aria-host-agent-setup.exe /out/ && echo 'OK -> /out/aria-host-agent.exe + /out/aria-host-agent-setup.exe'"]
|
||||
+43
-1
@@ -46,11 +46,23 @@ PyInstaller kann **nicht cross-kompilieren** — jede Binary wird auf ihrem OS g
|
||||
./build-native.sh # -> dist/aria-host-agent
|
||||
```
|
||||
|
||||
**Windows** — PyInstaller (Python 3 im PATH nötig):
|
||||
**Windows — nativ** (auf einem Windows-Rechner, Python 3 im PATH nötig):
|
||||
```bat
|
||||
build-native.bat REM -> dist\aria-host-agent.exe
|
||||
```
|
||||
|
||||
**Windows — aus Docker heraus (auf Linux!), inkl. Installer** — Wine baut die
|
||||
`.exe`, NSIS packt ein `setup.exe`, das den Agent als Windows-Dienst einrichtet:
|
||||
```bash
|
||||
./build-win.sh [version]
|
||||
# -> dist/aria-host-agent.exe (Konsolen-Binary)
|
||||
# -> dist/aria-host-agent-setup.exe (Installer: Dienst + .env in ProgramData)
|
||||
```
|
||||
Der erste Lauf zieht das `tobix/pywine`-Image (~1–2 GB) und richtet die Wine-
|
||||
Python-Umgebung ein — das dauert; Folge-Builds sind schnell. PyInstaller kann
|
||||
nicht cross-compilen, deshalb der Wine-Umweg. **macOS geht so NICHT** (Apple
|
||||
lässt sich nicht legal aus Docker bauen) — dort `./build-native.sh` auf einem Mac.
|
||||
|
||||
### Docker scheitert? (Live-ISO / overlayfs-Root)
|
||||
|
||||
Wenn `build.sh` mit `failed to mount … overlayfs … invalid argument` abbricht,
|
||||
@@ -96,6 +108,36 @@ Danach: `systemctl status aria-host-agent` · `journalctl -u aria-host-agent -f`
|
||||
Die Binary sucht er unter `dist/aria-host-agent` bzw. `./aria-host-agent` (oder
|
||||
2. Argument). Braucht `dialog` für den Dateibrowser (bietet die Installation an).
|
||||
|
||||
### Windows-Dienst (setup.exe)
|
||||
|
||||
`aria-host-agent-setup.exe` (aus `build-win.sh` oder dem Gitea-Release) als
|
||||
Administrator ausführen. Der Installer:
|
||||
- kopiert die `.exe` nach `%ProgramFiles%\ARIA Host-Agent`,
|
||||
- legt `%ProgramData%\ARIA-Host-Agent\.env` an (nur falls noch keine da ist),
|
||||
- richtet über **nssm** den Dienst **ARIA Host-Agent** ein (Autostart) und startet ihn.
|
||||
|
||||
Danach die `.env` unter `%ProgramData%\ARIA-Host-Agent\` mit RVS-Zugang +
|
||||
`CONTROL_ENABLED=true` füllen und den Dienst neu starten (`services.msc` →
|
||||
*ARIA Host-Agent*, oder `nssm restart ARIAHostAgent`). Deinstallation über
|
||||
*Apps & Features* → *ARIA Host-Agent* (die `.env` in ProgramData bleibt erhalten).
|
||||
|
||||
## Release (Binaries als Gitea-Assets)
|
||||
|
||||
`release_agent.sh <version>` baut alles Docker-Baubare und hängt es als
|
||||
**Release-Asset** an den Tag `agent-v<version>` — nichts landet im Git-Tree:
|
||||
|
||||
```bash
|
||||
./release_agent.sh 0.2.0 # Linux + Android + Windows (Wine)
|
||||
SKIP_WINDOWS=1 ./release_agent.sh 0.2.0 # ohne Windows (schneller)
|
||||
```
|
||||
|
||||
Assets: `aria-host-agent-linux-x64`, `aria-host-agent-android-agent-v<v>.apk`,
|
||||
`aria-host-agent-windows.exe`, `aria-host-agent-windows-setup.exe`. **macOS** ist
|
||||
nicht Docker-baubar — auf einem Mac `./build-native.sh` laufen lassen und das
|
||||
Ergebnis vor dem Release nach `dist/aria-host-agent-macos` legen, dann nimmt das
|
||||
Skript es automatisch mit. Gitea-Zugang via `.env`/Umgebung (`GITEA_URL`,
|
||||
`GITEA_REPO`, `GITEA_USER`), Kennwort wird abgefragt.
|
||||
|
||||
## TLS / SNI — Agent im selben Netz wie der RVS
|
||||
|
||||
Steht der Rechner im **selben Netz wie der RVS** (z.B. Rechenzentrum) und soll
|
||||
|
||||
@@ -6,3 +6,6 @@ local.properties
|
||||
.idea/
|
||||
*.iml
|
||||
captures/
|
||||
|
||||
# Signaturschluessel — NUR lokal, niemals ins oeffentliche Repo (Backup machen!)
|
||||
aria-agent.keystore
|
||||
|
||||
@@ -116,16 +116,79 @@ identisch, nur der Wecker ändert sich.
|
||||
4. **Feinschliff** — `info`/`app_list`/`notify`, Build-Härtung, `release.sh`
|
||||
(Version-Param → Gitea-Release-Asset, wie die App).
|
||||
|
||||
## Build & Release (geplant)
|
||||
## Bauen
|
||||
|
||||
APK-Builds laufen **nur unter Linux** — deshalb im Docker-Container. Du brauchst
|
||||
nichts Android-spezifisches installiert, **nur Docker**. Android-SDK, Gradle und
|
||||
Build-Tools zieht der Container selbst (`Dockerfile.build`).
|
||||
|
||||
```bash
|
||||
cd host-agent/android
|
||||
./build.sh # Docker (Android-SDK+Gradle) -> aria-android-agent.apk
|
||||
./release.sh 0.1.0 # baut, taggt, laedt APK als Gitea-Release-Asset hoch
|
||||
./build.sh
|
||||
```
|
||||
|
||||
APK wird manuell aufs Zielgerät kopiert + installiert (unbekannte Quellen
|
||||
erlauben). Gitea-Release macht den Download einfach (wie die Haupt-App).
|
||||
`build.sh` baut das Image `aria-android-agent-build` und lässt darin
|
||||
`gradle assembleDebug` laufen. Ergebnis:
|
||||
|
||||
> Status: **M1 + M2 fertig** (verbinden, `info`, `screenshot`, `ui_dump`).
|
||||
> Als Nächstes Meilenstein 3 (Steuern).
|
||||
```
|
||||
host-agent/android/dist/aria-android-agent.apk
|
||||
```
|
||||
|
||||
Das ist ein **Debug-APK**: auto-signiert mit dem Android-Debug-Key, also direkt
|
||||
installierbar — ohne eigenen Keystore, ohne Play Store.
|
||||
|
||||
### Auf dem Handy installieren
|
||||
|
||||
1. `dist/aria-android-agent.apk` aufs Zielgerät kopieren (USB, Cloud, `adb install
|
||||
dist/aria-android-agent.apk`, …).
|
||||
2. Antippen → Android fragt nach **„Unbekannte Quellen / Aus dieser Quelle
|
||||
installieren erlauben"** → erlauben.
|
||||
3. App öffnen → verbinden (QR/manuell), „Steuerung erlauben" an, für M2 zusätzlich
|
||||
„Bildschirm-Zugriff erlauben" + „Bedienungshilfe öffnen".
|
||||
|
||||
### Was der erste Build kostet
|
||||
|
||||
Der **erste** Lauf lädt viel (JDK-Image, Gradle, Android-SDK, Dependencies) und
|
||||
dauert entsprechend — mehrere Minuten. Folge-Builds sind schnell (Docker-Layer +
|
||||
Gradle-Cache im Image). Häufige Stolperer:
|
||||
|
||||
- **Docker fehlt / kein Zugriff** → `docker`-Rechte prüfen (`docker ps`).
|
||||
- **Overlay-on-Overlay auf einem Live-ISO** („invalid argument" beim Image-Bau) —
|
||||
gleiches Problem wie beim Desktop-Agent auf dem Mint-Live-System; von einer
|
||||
installierten Linux-Kiste bauen.
|
||||
- **Kotlin-/Manifest-Fehler** beim allerersten Bau eines neuen Meilensteins: den
|
||||
Gradle-Fehler posten, das glätten wir schnell.
|
||||
|
||||
### Version setzen
|
||||
|
||||
Bis es `release.sh` gibt, wird die Version in `app/build.gradle` gepflegt
|
||||
(`versionCode` / `versionName`). Aktuell `1` / `0.2.0` (M1+M2).
|
||||
|
||||
## Release
|
||||
|
||||
Das APK ist **kein** Teil des Git-Trees (blaeht sonst die History dauerhaft auf) —
|
||||
es wird als **Release-Asset** an einen Tag gehaengt. Das macht `release_agent.sh`
|
||||
(liegt eine Ebene hoeher, in `host-agent/`):
|
||||
|
||||
```bash
|
||||
cd host-agent
|
||||
./release_agent.sh 0.2.0
|
||||
```
|
||||
|
||||
Das Skript (wie die `release.sh` der Haupt-App, Version als Parameter):
|
||||
|
||||
- setzt die Version (`host_agent.py` → `AGENT_VERSION`, `app/build.gradle` →
|
||||
`versionName`/`versionCode`),
|
||||
- baut **Linux-Binary + Android-APK** per Docker,
|
||||
- committet den Version-Bump, taggt **`agent-v<version>`** (eigener Namespace,
|
||||
kollidiert nicht mit den App-Tags `v<version>`) und pusht,
|
||||
- legt ein Gitea-Release an und laedt die Assets hoch:
|
||||
`aria-host-agent-linux-x64`, `aria-host-agent-android-agent-v<version>.apk`,
|
||||
optional `-macos` / `-windows.exe` (falls in `host-agent/dist/` vorgebaut).
|
||||
|
||||
Gitea-Zugang (`GITEA_URL`, `GITEA_REPO`, `GITEA_USER`) kommt aus der Umgebung oder
|
||||
einer `.env`; das Kennwort wird interaktiv abgefragt. **Binaries landen unter
|
||||
„Releases", nie im Tree.**
|
||||
|
||||
> Status: **M1 + M2 fertig** (verbinden, `info`, `screenshot`, `ui_dump`),
|
||||
> `release_agent.sh` vorhanden. Als Nächstes Meilenstein 3 (Steuern).
|
||||
|
||||
@@ -11,13 +11,33 @@ android {
|
||||
applicationId 'de.hackersoft.ariaagent'
|
||||
minSdk 26
|
||||
targetSdk 33 // 33 vermeidet die Foreground-Service-Typ-Pflicht von 34
|
||||
versionCode 1
|
||||
versionName '0.1.0'
|
||||
versionCode 5
|
||||
versionName '0.0.0.5'
|
||||
}
|
||||
|
||||
// Fester Signaturschlüssel: jeder Build signiert mit DEMSELBEN Key, damit
|
||||
// Android neue APKs als Update derselben App akzeptiert (sonst "Konflikt mit
|
||||
// bestehendem Paket"). Die Keystore-Datei liegt NUR lokal (gitignored, nicht
|
||||
// im oeffentlichen Repo) — UNBEDINGT sichern, sonst brechen kuenftige Updates.
|
||||
def ariaKeystore = rootProject.file('aria-agent.keystore')
|
||||
signingConfigs {
|
||||
aria {
|
||||
if (ariaKeystore.exists()) {
|
||||
storeFile ariaKeystore
|
||||
storePassword 'ariaagent'
|
||||
keyAlias 'aria'
|
||||
keyPassword 'ariaagent'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
if (ariaKeystore.exists()) signingConfig signingConfigs.aria
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
if (ariaKeystore.exists()) signingConfig signingConfigs.aria
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
@@ -30,7 +32,7 @@
|
||||
<service
|
||||
android:name=".AgentService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
android:foregroundServiceType="dataSync|mediaProjection" />
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
@@ -43,15 +44,56 @@ class AgentService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createChannel()
|
||||
startForeground(NOTIF_ID, buildNotification("startet …"))
|
||||
startForegroundDataSync("startet …")
|
||||
}
|
||||
|
||||
/** Normaler Vordergrund-Start (Typ dataSync). mediaProjection wird NUR beim
|
||||
* Projizieren gesetzt — auf Android 14 darf man diesen Typ sonst nicht ohne
|
||||
* Projection-Token verwenden. */
|
||||
private fun startForegroundDataSync(text: String) {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
startForeground(NOTIF_ID, buildNotification(text),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
startForeground(NOTIF_ID, buildNotification(text))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
// WICHTIG: Jeder startForegroundService()-Aufruf MUSS binnen ~5s mit
|
||||
// startForeground() beantwortet werden — sonst crasht Android den Prozess
|
||||
// (ForegroundServiceDidNotStartInTimeException). Der Projection-Intent kommt
|
||||
// per startForegroundService, obwohl der Dienst schon laeuft -> hier IMMER
|
||||
// zuerst startForeground aufrufen (idempotent).
|
||||
startForegroundDataSync(status)
|
||||
|
||||
if (intent?.action == ACTION_PROJECTION) {
|
||||
val code = intent.getIntExtra(EXTRA_RESULT_CODE, 0)
|
||||
@Suppress("DEPRECATION")
|
||||
val data = intent.getParcelableExtra<Intent>(EXTRA_RESULT_DATA)
|
||||
if (code != 0 && data != null) ScreenCapturer.start(applicationContext, code, data)
|
||||
if (code != 0 && data != null) {
|
||||
try {
|
||||
// Android verlangt beim Projizieren einen Vordergrund-Dienst vom
|
||||
// Typ mediaProjection — VOR getMediaProjection() setzen, sonst
|
||||
// SecurityException (FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION).
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
startForeground(
|
||||
NOTIF_ID, buildNotification("Bildschirm-Zugriff aktiv"),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION or
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
}
|
||||
ScreenCapturer.start(applicationContext, code, data)
|
||||
if (ScreenCapturer.active) {
|
||||
updateNotification("$status · Bildschirm-Zugriff aktiv")
|
||||
} else {
|
||||
updateNotification("Bildschirm-Fehler: ${ScreenCapturer.lastError}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ScreenCapturer.lastError = "${e.javaClass.simpleName}: ${e.message}"
|
||||
updateNotification("Bildschirm-Fehler: ${ScreenCapturer.lastError}")
|
||||
}
|
||||
}
|
||||
if (rvs == null) startRvs() // Dienst war frisch -> Verbindung nachziehen
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@@ -126,16 +126,23 @@ class RvsClient(
|
||||
&& !target.equals(config.displayName(), true)) return
|
||||
|
||||
val action = payload.optString("action")
|
||||
val result: JSONObject = when {
|
||||
!config.controlEnabled ->
|
||||
err("Steuerung ist in der Agent-App deaktiviert (Schalter 'Steuerung erlauben').")
|
||||
action == "info" -> doInfo()
|
||||
action == "screenshot" -> doScreenshot()
|
||||
action == "ui_dump" -> doUiDump()
|
||||
action in listOf("ui_tap", "ui_text", "ui_swipe", "ui_key",
|
||||
"app_launch", "app_list", "notify") ->
|
||||
err("Aktion '$action' kommt in Meilenstein 3 (noch nicht implementiert).")
|
||||
else -> err("Aktion '$action' unbekannt.")
|
||||
// WICHTIG: JEDE Aktion muss ein host_result liefern — auch bei Exception
|
||||
// ODER OutOfMemoryError (Throwable!). Sonst bekommt ARIA statt einer
|
||||
// Fehlermeldung nur einen Timeout (kein Result kommt zurueck).
|
||||
val result: JSONObject = try {
|
||||
when {
|
||||
!config.controlEnabled ->
|
||||
err("Steuerung ist in der Agent-App deaktiviert (Schalter 'Steuerung erlauben').")
|
||||
action == "info" -> doInfo()
|
||||
action == "screenshot" -> doScreenshot()
|
||||
action == "ui_dump" -> doUiDump()
|
||||
action in listOf("ui_tap", "ui_text", "ui_swipe", "ui_key",
|
||||
"app_launch", "app_list", "notify") ->
|
||||
err("Aktion '$action' kommt in Meilenstein 3 (noch nicht implementiert).")
|
||||
else -> err("Aktion '$action' unbekannt.")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
err("Fehler bei '$action': ${t.javaClass.simpleName}: ${t.message}")
|
||||
}
|
||||
result.put("requestId", payload.optString("requestId"))
|
||||
result.put("hostId", config.hostId())
|
||||
@@ -147,11 +154,13 @@ class RvsClient(
|
||||
|
||||
/** Bildschirmfoto — selber Vertrag wie der Desktop-Agent: {format,bytes,base64}. */
|
||||
private fun doScreenshot(): JSONObject {
|
||||
if (!ScreenCapturer.active)
|
||||
if (!ScreenCapturer.active) {
|
||||
val why = ScreenCapturer.lastError?.let { " (letzter Fehler: $it)" } ?: ""
|
||||
return err("Bildschirm-Zugriff nicht erlaubt. In der Agent-App auf dem Handy " +
|
||||
"einmalig 'Bildschirm-Zugriff erlauben' antippen.")
|
||||
"einmalig 'Bildschirm-Zugriff erlauben' antippen.$why")
|
||||
}
|
||||
val png = ScreenCapturer.capture()
|
||||
?: return err("Screenshot fehlgeschlagen (kein Frame). Ist der Bildschirm an?")
|
||||
?: return err("Screenshot fehlgeschlagen: ${ScreenCapturer.lastError ?: "unbekannt"}")
|
||||
val b64 = android.util.Base64.encodeToString(png, android.util.Base64.NO_WRAP)
|
||||
val res = JSONObject().put("format", "png").put("bytes", png.size).put("base64", b64)
|
||||
return JSONObject().put("ok", true).put("result", res)
|
||||
|
||||
@@ -23,10 +23,16 @@ import java.io.ByteArrayOutputStream
|
||||
* Danach laeuft ein stiller VirtualDisplay -> ImageReader, aus dem `capture()`
|
||||
* bei Bedarf das aktuelle Bild als PNG zieht. Kein Google-Dienst.
|
||||
*
|
||||
* Aufgenommen wird direkt in GEKAPPTER Aufloesung (max. 1280 lange Seite): die
|
||||
* MediaProjection skaliert den Bildschirminhalt auf die VirtualDisplay-Groesse.
|
||||
* Das haelt Speicher/Zeit klein (kein 10-MB-Vollbild-Bitmap -> kein OOM/Timeout).
|
||||
*
|
||||
* Der Zugriff geht bei App-Kill / Neustart verloren und muss neu erlaubt werden
|
||||
* (Android-Sicherheit — Projection-Token ist nicht persistierbar).
|
||||
*/
|
||||
object ScreenCapturer {
|
||||
private const val MAX_SIDE = 1280
|
||||
|
||||
private var projection: MediaProjection? = null
|
||||
private var reader: ImageReader? = null
|
||||
private var vdisplay: VirtualDisplay? = null
|
||||
@@ -36,6 +42,10 @@ object ScreenCapturer {
|
||||
private var h = 0
|
||||
private var dpi = 0
|
||||
|
||||
/** Letzter Init-/Capture-Fehler (fuer die Fehlermeldung an ARIA). */
|
||||
@Volatile
|
||||
var lastError: String? = null
|
||||
|
||||
val active: Boolean
|
||||
@Synchronized get() = projection != null
|
||||
|
||||
@@ -43,31 +53,41 @@ object ScreenCapturer {
|
||||
fun start(ctx: Context, resultCode: Int, data: Intent) {
|
||||
stop()
|
||||
val mpm = ctx.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val mp = mpm.getMediaProjection(resultCode, data) ?: return
|
||||
val mp = mpm.getMediaProjection(resultCode, data) ?: run {
|
||||
lastError = "getMediaProjection lieferte null"
|
||||
return
|
||||
}
|
||||
|
||||
val metrics = DisplayMetrics()
|
||||
val wm = ctx.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
@Suppress("DEPRECATION")
|
||||
wm.defaultDisplay.getRealMetrics(metrics)
|
||||
w = metrics.widthPixels
|
||||
h = metrics.heightPixels
|
||||
dpi = metrics.densityDpi
|
||||
// Direkt gekappt aufnehmen (lange Seite <= MAX_SIDE), Seitenverhaeltnis wahren.
|
||||
val longSide = maxOf(metrics.widthPixels, metrics.heightPixels)
|
||||
val scale = if (longSide > MAX_SIDE) MAX_SIDE.toFloat() / longSide else 1f
|
||||
w = (metrics.widthPixels * scale).toInt().coerceAtLeast(1)
|
||||
h = (metrics.heightPixels * scale).toInt().coerceAtLeast(1)
|
||||
|
||||
handlerThread = HandlerThread("aria-capture").also { it.start() }
|
||||
handler = Handler(handlerThread!!.looper)
|
||||
|
||||
// Ab Android 14 Pflicht VOR createVirtualDisplay; frueher unschaedlich.
|
||||
mp.registerCallback(object : MediaProjection.Callback() {
|
||||
override fun onStop() { stop() }
|
||||
}, handler)
|
||||
|
||||
val ir = ImageReader.newInstance(w, h, PixelFormat.RGBA_8888, 2)
|
||||
reader = ir
|
||||
// AUTO_MIRROR = Standard-Flag fuer MediaProjection-Capture (die Projection
|
||||
// selbst autorisiert die Aufnahme, kein Sonderrecht noetig).
|
||||
vdisplay = mp.createVirtualDisplay(
|
||||
"aria-screen", w, h, dpi,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
ir.surface, null, handler,
|
||||
)
|
||||
projection = mp
|
||||
lastError = null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -83,50 +103,47 @@ object ScreenCapturer {
|
||||
handler = null
|
||||
}
|
||||
|
||||
/** PNG-Bytes des aktuellen Bildschirms, oder null. Wartet kurz auf einen Frame. */
|
||||
/** PNG-Bytes des aktuellen Bildschirms, oder null (Grund in lastError). */
|
||||
fun capture(): ByteArray? {
|
||||
val r = reader ?: return null
|
||||
val r = reader ?: run { lastError = "kein aktiver Bildschirm-Reader"; return null }
|
||||
var image: Image? = null
|
||||
var tries = 0
|
||||
while (tries < 20) {
|
||||
// Bis ~3s auf den ersten Frame warten (frischer VirtualDisplay braucht evtl. kurz).
|
||||
while (tries < 37) {
|
||||
image = r.acquireLatestImage()
|
||||
if (image != null) break
|
||||
try { Thread.sleep(80) } catch (_: InterruptedException) {}
|
||||
tries++
|
||||
}
|
||||
if (image == null) return null
|
||||
if (image == null) {
|
||||
lastError = "kein Frame erhalten (Bildschirm an? evtl. DRM-geschuetzter Inhalt)"
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
val plane = image.planes[0]
|
||||
val buffer = plane.buffer
|
||||
val pixelStride = plane.pixelStride
|
||||
val rowStride = plane.rowStride
|
||||
val rowPadding = rowStride - pixelStride * w
|
||||
val padded = Bitmap.createBitmap(
|
||||
w + (if (pixelStride > 0) rowPadding / pixelStride else 0),
|
||||
h, Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
padded.copyPixelsFromBuffer(buffer)
|
||||
val cropped = if (rowPadding == 0) padded else Bitmap.createBitmap(padded, 0, 0, w, h)
|
||||
val scaled = downscale(cropped, 1280)
|
||||
val bmpW = w + (if (pixelStride > 0) rowPadding / pixelStride else 0)
|
||||
val bmp = Bitmap.createBitmap(bmpW, h, Bitmap.Config.ARGB_8888)
|
||||
bmp.copyPixelsFromBuffer(buffer)
|
||||
val out = ByteArrayOutputStream()
|
||||
scaled.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
if (scaled !== cropped) scaled.recycle()
|
||||
if (cropped !== padded) cropped.recycle()
|
||||
padded.recycle()
|
||||
if (rowPadding == 0) {
|
||||
bmp.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
} else {
|
||||
val cropped = Bitmap.createBitmap(bmp, 0, 0, w, h)
|
||||
cropped.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
cropped.recycle()
|
||||
}
|
||||
bmp.recycle()
|
||||
lastError = null
|
||||
out.toByteArray()
|
||||
} catch (_: Exception) {
|
||||
} catch (t: Throwable) {
|
||||
lastError = "Encode-Fehler: ${t.javaClass.simpleName}: ${t.message}"
|
||||
null
|
||||
} finally {
|
||||
try { image?.close() } catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downscale(src: Bitmap, maxSide: Int): Bitmap {
|
||||
val longSide = maxOf(src.width, src.height)
|
||||
if (longSide <= maxSide) return src
|
||||
val scale = maxSide.toFloat() / longSide
|
||||
return Bitmap.createScaledBitmap(
|
||||
src, (src.width * scale).toInt(), (src.height * scale).toInt(), true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Baut die Windows-.exe + setup.exe des Host-Agents AUF LINUX (Wine im Docker).
|
||||
# ./build-win.sh [version]
|
||||
# Ergebnis:
|
||||
# dist/aria-host-agent.exe (Konsolen-Binary)
|
||||
# dist/aria-host-agent-setup.exe (Installer: richtet Windows-Dienst ein)
|
||||
#
|
||||
# Hinweis: Der erste Lauf zieht das tobix/pywine-Image (~1-2 GB) + baut die
|
||||
# Wine-Umgebung — das dauert. Folge-Builds sind schnell.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
VERSION="${1:-0.0.0}"
|
||||
mkdir -p dist
|
||||
docker build -f Dockerfile.win --build-arg VERSION="$VERSION" -t aria-host-agent-win .
|
||||
docker run --rm -v "$(pwd)/dist:/out" aria-host-agent-win
|
||||
echo
|
||||
echo "Fertig: dist/aria-host-agent.exe + dist/aria-host-agent-setup.exe"
|
||||
@@ -64,7 +64,13 @@ def _load_dotenv() -> None:
|
||||
"""Laedt eine .env neben der Binary/dem Script (oder im CWD) in os.environ.
|
||||
Bereits gesetzte Werte gewinnen. Kein python-dotenv noetig."""
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
for path in (os.path.join(here, ".env"), os.path.join(os.getcwd(), ".env")):
|
||||
# PyInstaller-onefile: __file__ liegt im Temp-Extract-Dir, NICHT beim .exe/
|
||||
# Binary — deshalb zusaetzlich sys.executable-Ordner (echter Binary-Ort) und
|
||||
# das CWD (z.B. der ProgramData-Ordner, den der Windows-Dienst als AppDir nutzt).
|
||||
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
seen = set()
|
||||
candidates = [os.path.join(d, ".env") for d in (exe_dir, here, os.getcwd())]
|
||||
for path in [p for p in candidates if not (p in seen or seen.add(p))]:
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
try:
|
||||
@@ -155,6 +161,9 @@ OUT_MAX_CHARS_HARD = int(os.environ.get("OUT_MAX_CHARS_HARD", "200000") or "2000
|
||||
# Datei-Transfer-Limit (Base64 durchs RVS).
|
||||
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).
|
||||
AGENT_VERSION = "0.0.0.5"
|
||||
|
||||
HEARTBEAT_SEC = 25
|
||||
CAPS = ["exec", "read", "write", "info", "screenshot"]
|
||||
|
||||
@@ -291,6 +300,7 @@ def _do_write(params: dict) -> dict:
|
||||
def _do_info(params: dict) -> dict:
|
||||
info = {
|
||||
"host": HOST_NAME, "hostname": socket.gethostname(),
|
||||
"agent_version": AGENT_VERSION,
|
||||
"os": platform.platform(), "kernel": platform.release(),
|
||||
"arch": platform.machine(), "python": platform.python_version(),
|
||||
"user": os.environ.get("USER") or os.environ.get("USERNAME") or "",
|
||||
@@ -414,7 +424,7 @@ class HostAgent:
|
||||
HOST_ID, HOST_NAME, ",".join(CAPS), CONTROL_ENABLED)
|
||||
await self._send({"type": "host_hello", "payload": {
|
||||
"hostId": HOST_ID, "name": HOST_NAME, "os": platform.platform(),
|
||||
"caps": CAPS, "control": CONTROL_ENABLED,
|
||||
"version": AGENT_VERSION, "caps": CAPS, "control": CONTROL_ENABLED,
|
||||
}, "timestamp": int(time.time() * 1000)})
|
||||
|
||||
async def _heartbeat(self) -> None:
|
||||
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# ARIA Host-Agent — Release Script
|
||||
# Baut die auf Linux+Docker moeglichen Artefakte und haengt sie als
|
||||
# Gitea-Release-Assets an einen Tag. NICHTS wandert in den Git-Tree —
|
||||
# Binaries leben nur unter "Releases" (blaeht clone/History nicht auf).
|
||||
#
|
||||
# Verwendung: ./release_agent.sh <version> (z.B. ./release_agent.sh 0.2.0)
|
||||
#
|
||||
# Artefakte:
|
||||
# - Linux-x64-Binary (Docker, PyInstaller) -> immer
|
||||
# - Android-APK (Docker, Gradle) -> immer
|
||||
# - macOS / Windows (falls in dist/ vorgebaut) -> optional
|
||||
# (mac/win koennen auf Linux nicht cross-gebaut werden -> build-native.*
|
||||
# auf dem jeweiligen OS laufen lassen, Ergebnis nach host-agent/dist/ legen)
|
||||
#
|
||||
# Eigener Tag-Namespace agent-v<version> (kollidiert NICHT mit den
|
||||
# App-Tags v<version>).
|
||||
#
|
||||
# Gitea-Zugang (GITEA_URL, GITEA_REPO, GITEA_USER) wird aus der Umgebung
|
||||
# oder aus host-agent/.env bzw. der Repo-Wurzel-.env gelesen; das Kennwort
|
||||
# wird interaktiv abgefragt.
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")" # host-agent/
|
||||
SCRIPT_DIR="$(pwd)"
|
||||
ROOT_DIR="$(cd .. && pwd)"
|
||||
|
||||
# ── Farben ───────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
|
||||
# ── Parameter ────────────────────────────────────────────────────────
|
||||
VERSION=${1:?"Usage: ./release_agent.sh <version> (z.B. 0.2.0)"}
|
||||
TAG="agent-v$VERSION"
|
||||
|
||||
# ── Gitea-Konfiguration (env > host-agent/.env > Repo-Wurzel-.env) ────
|
||||
[ -f "$SCRIPT_DIR/.env" ] && source "$SCRIPT_DIR/.env"
|
||||
[ -f "$ROOT_DIR/.env" ] && source "$ROOT_DIR/.env"
|
||||
|
||||
GITEA_URL="${GITEA_URL:?"GITEA_URL nicht gesetzt (in .env oder als Umgebungsvariable)"}"
|
||||
GITEA_REPO="${GITEA_REPO:?"GITEA_REPO nicht gesetzt (z.B. stefan/aria-agent)"}"
|
||||
GITEA_USER="${GITEA_USER:-$(echo "$GITEA_REPO" | cut -d'/' -f1)}"
|
||||
|
||||
echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║ ARIA Host-Agent Release — ${TAG}$(printf '%*s' $((14 - ${#TAG})) '')║${NC}"
|
||||
echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}\n"
|
||||
|
||||
# ── Kennwort ─────────────────────────────────────────────────────────
|
||||
echo -e "${YELLOW}Gitea-Login: ${GITEA_USER}${NC}"
|
||||
read -s -p "Gitea-Kennwort: " GITEA_PASS; echo ""
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -u "${GITEA_USER}:${GITEA_PASS}" "$GITEA_URL/api/v1/user")
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo -e "${RED}Login fehlgeschlagen (HTTP $HTTP_CODE). Kennwort korrekt?${NC}"; exit 1
|
||||
fi
|
||||
echo -e " ${GREEN}✓${NC} Login erfolgreich\n"
|
||||
|
||||
# ── Versionsnummern setzen ───────────────────────────────────────────
|
||||
echo -e "${GREEN}[1/5] Version auf $VERSION setzen...${NC}"
|
||||
# Desktop-Agent
|
||||
sed -i "s/^AGENT_VERSION = \"[^\"]*\"/AGENT_VERSION = \"$VERSION\"/" host_agent.py
|
||||
echo -e " ${GREEN}✓${NC} host_agent.py → AGENT_VERSION $VERSION"
|
||||
# Android: versionName + versionCode (aus Version berechnen; 3- oder 4-stellig)
|
||||
IFS='.' read -ra VP <<< "$VERSION"
|
||||
V1=${VP[0]:-0}; V2=${VP[1]:-0}; V3=${VP[2]:-0}; V4=${VP[3]:-0}
|
||||
VERSION_CODE=$((V1 * 1000000 + V2 * 10000 + V3 * 100 + V4)); [ "$VERSION_CODE" -lt 1 ] && VERSION_CODE=1
|
||||
sed -i "s/versionName '[^']*'/versionName '$VERSION'/" android/app/build.gradle
|
||||
sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/app/build.gradle
|
||||
echo -e " ${GREEN}✓${NC} android/app/build.gradle → versionName $VERSION, versionCode $VERSION_CODE\n"
|
||||
|
||||
# ── Bauen (Docker) ───────────────────────────────────────────────────
|
||||
echo -e "${GREEN}[2/5] Linux-Binary + Android-APK bauen (Docker)...${NC}"
|
||||
./build.sh >/dev/null
|
||||
LINUX_BIN="$SCRIPT_DIR/dist/aria-host-agent"
|
||||
[ -f "$LINUX_BIN" ] || { echo -e "${RED}Linux-Binary fehlt: $LINUX_BIN${NC}"; exit 1; }
|
||||
echo -e " ${GREEN}✓${NC} Linux-Binary ($(du -h "$LINUX_BIN" | cut -f1))"
|
||||
|
||||
( cd android && ./build.sh >/dev/null )
|
||||
APK="$SCRIPT_DIR/android/dist/aria-android-agent.apk"
|
||||
[ -f "$APK" ] || { echo -e "${RED}APK fehlt: $APK${NC}"; exit 1; }
|
||||
echo -e " ${GREEN}✓${NC} Android-APK ($(du -h "$APK" | cut -f1))"
|
||||
|
||||
# Windows (.exe + setup.exe) via Wine im Docker. Dauert (pywine-Image ~1-2 GB) —
|
||||
# mit SKIP_WINDOWS=1 ./release_agent.sh <v> ueberspringbar.
|
||||
if [ "${SKIP_WINDOWS:-0}" = "1" ]; then
|
||||
echo -e " ${YELLOW}Windows-Build uebersprungen (SKIP_WINDOWS=1)${NC}"
|
||||
else
|
||||
echo -e " ${CYAN}…${NC} Windows-.exe + setup.exe bauen (Wine, kann dauern)"
|
||||
if ./build-win.sh "$VERSION" >/dev/null 2>&1; then
|
||||
echo -e " ${GREEN}✓${NC} Windows-.exe + setup.exe"
|
||||
else
|
||||
echo -e " ${YELLOW}Windows-Build fehlgeschlagen — Release laeuft ohne Windows weiter.${NC}"
|
||||
echo -e " ${YELLOW}(Einzeln testen: ./build-win.sh $VERSION)${NC}"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Asset-Liste aufbauen: "lokaler_pfad::asset-name"
|
||||
ASSETS=(
|
||||
"$LINUX_BIN::aria-host-agent-linux-x64"
|
||||
"$APK::aria-host-agent-android-$TAG.apk"
|
||||
)
|
||||
# Native Artefakte (nur wenn vorhanden): Windows aus build-win.sh, macOS
|
||||
# vorgebaut (build-native.sh auf einem Mac -> dist/aria-host-agent-macos legen).
|
||||
[ -f "$SCRIPT_DIR/dist/aria-host-agent-macos" ] && ASSETS+=("$SCRIPT_DIR/dist/aria-host-agent-macos::aria-host-agent-macos")
|
||||
[ -f "$SCRIPT_DIR/dist/aria-host-agent.exe" ] && ASSETS+=("$SCRIPT_DIR/dist/aria-host-agent.exe::aria-host-agent-windows.exe")
|
||||
[ -f "$SCRIPT_DIR/dist/aria-host-agent-setup.exe" ] && ASSETS+=("$SCRIPT_DIR/dist/aria-host-agent-setup.exe::aria-host-agent-windows-setup.exe")
|
||||
|
||||
# ── Git-Tag ──────────────────────────────────────────────────────────
|
||||
echo -e "${GREEN}[3/5] Git-Tag $TAG...${NC}"
|
||||
git add host_agent.py android/app/build.gradle
|
||||
git commit -m "release(agent): bump to $VERSION" 2>/dev/null || echo -e " ${YELLOW}Keine Aenderungen zum Committen${NC}"
|
||||
if git rev-parse "$TAG" &>/dev/null; then
|
||||
echo -e " ${YELLOW}Tag $TAG existiert bereits — überspringe${NC}"
|
||||
else
|
||||
git tag "$TAG"; echo -e " ${GREEN}✓${NC} Tag $TAG erstellt"
|
||||
fi
|
||||
git push origin main "$TAG"
|
||||
echo -e " ${GREEN}✓${NC} Tag gepusht\n"
|
||||
|
||||
# ── Gitea-Release ────────────────────────────────────────────────────
|
||||
echo -e "${GREEN}[4/5] Gitea-Release anlegen...${NC}"
|
||||
BODY=$(printf 'ARIA Host-Agent %s\n\nDesktop (Linux) + Android-APK. Auf das Zielgeraet kopieren, siehe README.' "$TAG")
|
||||
BODY_JSON=$(printf '%s' "$BODY" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null || printf '"%s"' "$BODY")
|
||||
RESP=$(curl -s -X POST "$GITEA_URL/api/v1/repos/$GITEA_REPO/releases" \
|
||||
-u "${GITEA_USER}:${GITEA_PASS}" -H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"Host-Agent $TAG\",\"body\":$BODY_JSON,\"draft\":false,\"prerelease\":false}")
|
||||
RELEASE_ID=$(echo "$RESP" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
if [ -z "$RELEASE_ID" ]; then echo -e "${RED}Release fehlgeschlagen:${NC}\n$RESP"; exit 1; fi
|
||||
echo -e " ${GREEN}✓${NC} Release #$RELEASE_ID erstellt\n"
|
||||
|
||||
# ── Assets hochladen ─────────────────────────────────────────────────
|
||||
echo -e "${GREEN}[5/5] Assets hochladen (${#ASSETS[@]})...${NC}"
|
||||
for entry in "${ASSETS[@]}"; do
|
||||
path="${entry%%::*}"; name="${entry##*::}"
|
||||
UP=$(curl -s -X POST \
|
||||
"$GITEA_URL/api/v1/repos/$GITEA_REPO/releases/$RELEASE_ID/assets?name=$name" \
|
||||
-u "${GITEA_USER}:${GITEA_PASS}" -F "attachment=@${path}")
|
||||
if echo "$UP" | grep -q '"name"'; then
|
||||
echo -e " ${GREEN}✓${NC} $name"
|
||||
else
|
||||
echo -e " ${RED}✗ $name fehlgeschlagen:${NC} $UP"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ Host-Agent $TAG ist live!${NC}"
|
||||
echo -e "${GREEN}║${NC} $GITEA_URL/$GITEA_REPO/releases/tag/$TAG"
|
||||
echo -e "${GREEN}╚═══════════════════════════════════════════════════╝${NC}"
|
||||
@@ -0,0 +1,83 @@
|
||||
; ARIA Host-Agent — Windows-Installer (NSIS, auf Linux mit makensis gebaut).
|
||||
;
|
||||
; Installiert die Agent-.exe nach %ProgramFiles%\ARIA Host-Agent, legt eine .env
|
||||
; in %ProgramData%\ARIA-Host-Agent an (nur falls noch keine da ist — User-Config
|
||||
; bleibt erhalten) und richtet einen automatisch startenden Windows-Dienst via
|
||||
; nssm ein. Der Dienst laeuft mit AppDirectory = ProgramData-Ordner, damit der
|
||||
; Agent die .env von dort (aus dem CWD) liest.
|
||||
|
||||
!ifndef VERSION
|
||||
!define VERSION "0.0.0"
|
||||
!endif
|
||||
!define SVC "ARIAHostAgent"
|
||||
|
||||
Name "ARIA Host-Agent ${VERSION}"
|
||||
OutFile "aria-host-agent-setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\ARIA Host-Agent"
|
||||
RequestExecutionLevel admin
|
||||
Unicode true
|
||||
ShowInstDetails show
|
||||
ShowUninstDetails show
|
||||
|
||||
Var DataDir
|
||||
|
||||
Page directory
|
||||
Page instfiles
|
||||
UninstPage uninstConfirm
|
||||
UninstPage instfiles
|
||||
|
||||
Section "Install"
|
||||
SetOutPath "$INSTDIR"
|
||||
File "aria-host-agent.exe"
|
||||
File "nssm.exe"
|
||||
|
||||
; ProgramData-Ordner fuer die .env bestimmen
|
||||
ReadEnvStr $0 "ProgramData"
|
||||
StrCmp $0 "" 0 +2
|
||||
StrCpy $0 "$PROFILE" ; Fallback, falls %ProgramData% fehlt
|
||||
StrCpy $DataDir "$0\ARIA-Host-Agent"
|
||||
CreateDirectory "$DataDir"
|
||||
|
||||
; .env nur schreiben, wenn noch keine existiert (User-Config nicht ueberschreiben)
|
||||
IfFileExists "$DataDir\.env" env_done 0
|
||||
FileOpen $1 "$DataDir\.env" w
|
||||
FileWrite $1 "# ARIA Host-Agent — Konfiguration (dieser Windows-Dienst liest diese Datei).$\r$\n"
|
||||
FileWrite $1 "# Nach dem Aendern den Dienst neu starten: services.msc -> ARIA Host-Agent.$\r$\n"
|
||||
FileWrite $1 "RVS_HOST=rvs.example.de$\r$\n"
|
||||
FileWrite $1 "RVS_PORT=443$\r$\n"
|
||||
FileWrite $1 "RVS_TLS=true$\r$\n"
|
||||
FileWrite $1 "RVS_TLS_FALLBACK=true$\r$\n"
|
||||
FileWrite $1 "RVS_TOKEN=$\r$\n"
|
||||
FileWrite $1 "RVS_SNI=$\r$\n"
|
||||
FileWrite $1 "HOST_NAME=$\r$\n"
|
||||
FileWrite $1 "CONTROL_ENABLED=true$\r$\n"
|
||||
FileClose $1
|
||||
env_done:
|
||||
|
||||
; Dienst (neu) einrichten — evtl. alten sauber entfernen, dann installieren
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" stop ${SVC}'
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" remove ${SVC} confirm'
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" install ${SVC} "$INSTDIR\aria-host-agent.exe"'
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" set ${SVC} AppDirectory "$DataDir"'
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" set ${SVC} DisplayName "ARIA Host-Agent"'
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" set ${SVC} Description "ARIA-Fernsteuerung dieses Rechners (RVS-Agent)."'
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" set ${SVC} Start SERVICE_AUTO_START'
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" start ${SVC}'
|
||||
|
||||
; Uninstaller + Eintrag unter "Apps & Features"
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ARIAHostAgent" "DisplayName" "ARIA Host-Agent"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ARIAHostAgent" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ARIAHostAgent" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" stop ${SVC}'
|
||||
nsExec::ExecToLog '"$INSTDIR\nssm.exe" remove ${SVC} confirm'
|
||||
Delete "$INSTDIR\aria-host-agent.exe"
|
||||
Delete "$INSTDIR\nssm.exe"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
RMDir "$INSTDIR"
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\ARIAHostAgent"
|
||||
; Die .env in %ProgramData%\ARIA-Host-Agent bleibt bewusst erhalten (User-Config).
|
||||
SectionEnd
|
||||
Reference in New Issue
Block a user