manage-client.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. #!/usr/bin/env php
  2. <?php
  3. declare(strict_types=1);
  4. // Command line interface for the manage client.
  5. //
  6. // Contains no logic of its own: every command calls the same public functions
  7. // the GUI panel and the host application use, so a feature behaves identically
  8. // however it is triggered.
  9. //
  10. // Usage:
  11. // php manage-client/bin/manage-client.php status
  12. // check
  13. // update [--force] [--yes] [--skip-hook]
  14. // migrate [--dry-run]
  15. // backup [--trigger=cron]
  16. // heartbeat
  17. //
  18. // Exit codes:
  19. // 0 success
  20. // 1 error (including a failed post-update step after a successful deploy)
  21. // 2 update available (check only)
  22. if (PHP_SAPI !== "cli") {
  23. http_response_code(403);
  24. exit("This script must be run from the command line.\n");
  25. }
  26. require_once dirname(__DIR__) . "/lib/client.php";
  27. $argv = $_SERVER["argv"] ?? [];
  28. array_shift($argv);
  29. $command = "";
  30. $flags = [];
  31. foreach ($argv as $argument) {
  32. if (str_starts_with($argument, "--")) {
  33. $parts = explode("=", substr($argument, 2), 2);
  34. $flags[$parts[0]] = $parts[1] ?? true;
  35. } elseif ($command === "") {
  36. $command = $argument;
  37. }
  38. }
  39. $quiet = isset($flags["quiet"]);
  40. function manageCliOut(string $line): void
  41. {
  42. global $quiet;
  43. if (!$quiet) {
  44. fwrite(STDOUT, $line . PHP_EOL);
  45. }
  46. }
  47. // Errors always print, even with --quiet, so a cron job still mails a failure.
  48. function manageCliError(string $line): void
  49. {
  50. fwrite(STDERR, $line . PHP_EOL);
  51. }
  52. function manageCliUsage(): void
  53. {
  54. fwrite(STDOUT, <<<TEXT
  55. Manage client
  56. status Übersicht: Version, Update, Backups, Migrationen
  57. check Prüft auf ein neues Release (Exit 2 = Update verfügbar)
  58. update [--force] [--yes] Spielt das aktuelle Release ein
  59. [--skip-hook] Nur Dateien ausrollen, ohne Migrationen/Hook
  60. migrate [--dry-run] Führt offene Migrationen aus
  61. backup [--trigger=cron] Erstellt ein Backup und lädt es hoch
  62. heartbeat Meldet den Status an den Manage-Server
  63. Optionen: --quiet unterdrückt die normale Ausgabe (Fehler weiterhin auf STDERR).
  64. TEXT);
  65. }
  66. function manageCliConfirm(string $question): bool
  67. {
  68. global $flags;
  69. if (isset($flags["yes"])) {
  70. return true;
  71. }
  72. fwrite(STDOUT, $question . " [j/N]: ");
  73. $answer = trim((string) fgets(STDIN));
  74. return in_array(strtolower($answer), ["j", "ja", "y", "yes"], true);
  75. }
  76. // Renders the migration/hook part of an update result. Shared by update and
  77. // migrate so both report a failure the same way.
  78. function manageCliReportHook(?array $hook): bool
  79. {
  80. if ($hook === null) {
  81. return true;
  82. }
  83. if (!empty($hook["skipped"])) {
  84. $pending = (int) ($hook["migrations"]["pending"] ?? 0);
  85. manageCliOut(" Post-Update übersprungen (--skip-hook)." .
  86. ($pending > 0 ? " Offene Migrationen: " . $pending : ""));
  87. return true;
  88. }
  89. $migrations = $hook["migrations"] ?? [];
  90. $applied = $migrations["applied"] ?? [];
  91. if ($applied !== []) {
  92. manageCliOut(" Migrationen ausgeführt: " . implode(", ", $applied));
  93. }
  94. if (!empty($hook["success"])) {
  95. if (!empty($hook["hook"]["configured"])) {
  96. manageCliOut(" Post-Update-Hook ausgeführt.");
  97. }
  98. return true;
  99. }
  100. if (!empty($hook["failed_migration"])) {
  101. manageCliError(" FEHLER in Migration " . $hook["failed_migration"] . ": " . (string) $hook["error"]);
  102. manageCliError(" Verbleibende Migrationen wurden NICHT ausgeführt.");
  103. manageCliError(" Ursache beheben und danach erneut ausführen: manage-client.php migrate");
  104. } else {
  105. manageCliError(" FEHLER im Post-Update-Hook: " . (string) ($hook["error"] ?? "unbekannt"));
  106. }
  107. return false;
  108. }
  109. try {
  110. switch ($command) {
  111. case "status":
  112. $status = manageClientStatus();
  113. manageCliOut("Instanz: " . ($status["instance"] !== "" ? $status["instance"] : "(nicht gesetzt)"));
  114. manageCliOut("Server: " . ($status["server_url"] !== "" ? $status["server_url"] : "(nicht gesetzt)"));
  115. manageCliOut("Konfiguriert: " . ($status["configured"] ? "ja" : "NEIN"));
  116. manageCliOut("Installierte Ver.: " . ($status["version"] !== "" ? $status["version"] : "unbekannt"));
  117. manageCliOut("PHP: " . $status["php_version"]);
  118. if ($status["update"] !== null) {
  119. manageCliOut("Aktuelles Release: " . $status["update"]["latest"]);
  120. manageCliOut("Update verfügbar: " . ($status["update"]["available"] ? "JA" : "nein"));
  121. } elseif ($status["update_error"] !== null) {
  122. manageCliOut("Update-Prüfung: fehlgeschlagen (" . $status["update_error"] . ")");
  123. }
  124. manageCliOut("Lokale Backups: " . count($status["backups"]));
  125. manageCliOut("Letztes Backup: " . ($status["last_backup_at"] ?? "nie"));
  126. manageCliOut("Offene Migrationen: " . count($status["pending_migrations"]));
  127. foreach ($status["pending_migrations"] as $migration) {
  128. manageCliOut(" - " . $migration["id"]);
  129. }
  130. foreach ($status["errors"] as $error) {
  131. manageCliError("Warnung: " . $error);
  132. }
  133. exit(0);
  134. case "check":
  135. $check = manageUpdateCheck();
  136. manageCliOut("Installiert: " . ($check["current"] !== "" ? $check["current"] : "unbekannt"));
  137. manageCliOut("Verfügbar: " . $check["latest"]);
  138. if ($check["available"]) {
  139. manageCliOut("Ein Update ist verfügbar.");
  140. exit(2);
  141. }
  142. manageCliOut("Die Installation ist aktuell.");
  143. exit(0);
  144. case "update":
  145. $check = manageUpdateCheck();
  146. $force = isset($flags["force"]);
  147. if (!$check["available"] && !$force) {
  148. manageCliOut("Kein Update verfügbar. Mit --force kann dasselbe Paket erneut ausgerollt werden.");
  149. exit(0);
  150. }
  151. if (!manageCliConfirm(
  152. "Version " . $check["latest"] . " jetzt ausrollen" .
  153. ($check["current"] !== "" ? " (installiert: " . $check["current"] . ")" : "") . "?"
  154. )) {
  155. manageCliOut("Abgebrochen.");
  156. exit(0);
  157. }
  158. $result = manageUpdateApply([
  159. "force" => $force,
  160. "skip_hook" => isset($flags["skip-hook"]),
  161. ]);
  162. manageCliOut("Update ausgerollt: " . $result["from_version"] . " -> " . $result["to_version"]);
  163. manageCliOut(" Dateien kopiert: " . $result["copied"]);
  164. manageCliOut(" Dateien gesichert: " . $result["backed_up"]);
  165. manageCliOut(" Geschützt übersprungen: " . $result["skipped"]);
  166. manageCliOut(" Sicherungsverzeichnis: " . $result["backup_dir"]);
  167. $hookOk = manageCliReportHook($result["hook"]);
  168. manageHeartbeatSendQuietly();
  169. // The deployment succeeded either way; the exit code reports the
  170. // post-update step so a cron job notices a failed migration.
  171. exit($hookOk ? 0 : 1);
  172. case "migrate":
  173. $pending = manageUpdatePendingMigrations();
  174. if ($pending === []) {
  175. manageCliOut("Keine offenen Migrationen.");
  176. exit(0);
  177. }
  178. manageCliOut("Offene Migrationen: " . count($pending));
  179. foreach ($pending as $migration) {
  180. manageCliOut(" - " . $migration["id"]);
  181. }
  182. if (isset($flags["dry-run"])) {
  183. manageCliOut("--dry-run: nichts ausgeführt.");
  184. exit(0);
  185. }
  186. if (!manageCliConfirm("Diese Migrationen jetzt ausführen?")) {
  187. manageCliOut("Abgebrochen.");
  188. exit(0);
  189. }
  190. $report = manageUpdateRunMigrations();
  191. if ($report["applied"] !== []) {
  192. manageCliOut("Ausgeführt: " . implode(", ", $report["applied"]));
  193. }
  194. if (!$report["success"]) {
  195. manageCliError("FEHLER in Migration " . (string) $report["failed"] . ": " . (string) $report["error"]);
  196. manageCliError("Verbleibende Migrationen: " . ($report["pending"] - 1));
  197. exit(1);
  198. }
  199. manageCliOut("Alle Migrationen abgeschlossen.");
  200. manageHeartbeatSendQuietly();
  201. exit(0);
  202. case "backup":
  203. $trigger = is_string($flags["trigger"] ?? null) ? (string) $flags["trigger"] : "manual";
  204. $record = manageBackupCreate($trigger);
  205. manageCliOut("Backup erstellt: " . $record["filename"]);
  206. manageCliOut(" Dateien: " . $record["file_count"]);
  207. manageCliOut(" Größe: " . manageFormatBytes((int) $record["size"]));
  208. manageCliOut(" SHA-256: " . $record["sha256"]);
  209. if (is_array($record["database"] ?? null)) {
  210. manageCliOut(" Datenbank: " . $record["database"]["tables"] . " Tabellen, " .
  211. $record["database"]["rows"] . " Zeilen");
  212. }
  213. $failed = false;
  214. foreach ($record["remote_uploads"] as $upload) {
  215. if (!empty($upload["success"])) {
  216. manageCliOut(" Upload " . $upload["target"] . ": OK");
  217. } else {
  218. $failed = true;
  219. manageCliError(" Upload " . $upload["target"] . " FEHLGESCHLAGEN: " .
  220. (string) ($upload["error"] ?? "unbekannt"));
  221. }
  222. }
  223. manageHeartbeatSendQuietly();
  224. // The local archive exists regardless, but a failed upload must be
  225. // visible to cron.
  226. exit($failed ? 1 : 0);
  227. case "heartbeat":
  228. $result = manageHeartbeatSend();
  229. manageCliOut("Heartbeat gesendet.");
  230. manageCliOut(" Aktuelles Release: " . ($result["latest"] !== "" ? $result["latest"] : "keines"));
  231. manageCliOut(" Update verfügbar: " . ($result["update_available"] ? "JA" : "nein"));
  232. exit(0);
  233. case "":
  234. case "help":
  235. case "-h":
  236. case "--help":
  237. manageCliUsage();
  238. exit(0);
  239. default:
  240. manageCliError("Unbekannter Befehl: " . $command);
  241. manageCliUsage();
  242. exit(1);
  243. }
  244. } catch (Throwable $exception) {
  245. manageCliError("Fehler: " . $exception->getMessage());
  246. exit(1);
  247. }