arozos-server.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. /*
  2. ArozOS test-instance launcher for the full-stack E2E specs.
  3. Boots a disposable, real ArozOS server (the Go binary built from src/)
  4. inside an isolated instance folder, so specs can exercise genuine
  5. authentication, desktop, file system and admin APIs without touching
  6. the developer's own runtime data under src/.
  7. Instance layout (recreated from scratch on every start):
  8. .instance/
  9. ├── web symlink to ../../../src/web (static assets, read-only)
  10. ├── system private copy of src/system (ao.db + runtime state)
  11. ├── files/ user home directories (created by the server)
  12. ├── tmp/ scratch space (created by the server)
  13. └── server.log combined stdout/stderr of the server process
  14. Because the instance is wiped each start, the server always boots in
  15. the zero-user state and bootstrapAdmin() creates a deterministic
  16. administrator account through the same public endpoint the first-boot
  17. wizard (user.html) posts to.
  18. Env:
  19. AROZOS_BIN path to a prebuilt arozos binary (default: src/arozos,
  20. built automatically with `go build` when missing)
  21. */
  22. "use strict";
  23. const path = require("path");
  24. const fs = require("fs");
  25. const { spawn, spawnSync } = require("child_process");
  26. const REPO_ROOT = path.resolve(__dirname, "../../../..");
  27. const SRC_DIR = path.join(REPO_ROOT, "src");
  28. const WEB_ROOT = path.join(SRC_DIR, "web");
  29. const SYSTEM_TEMPLATE = path.join(SRC_DIR, "system");
  30. const INSTANCE_DIR = path.resolve(__dirname, "../.instance");
  31. const DEFAULT_PORT = 8126;
  32. const ADMIN_USER = "admin";
  33. const ADMIN_PASS = "e2e-Admin-Passw0rd";
  34. function binaryName() {
  35. return process.platform === "win32" ? "arozos.exe" : "arozos";
  36. }
  37. // Locate the server binary, building it with `go build` when missing.
  38. function resolveBinary() {
  39. if (process.env.AROZOS_BIN && fs.existsSync(process.env.AROZOS_BIN)) {
  40. return path.resolve(process.env.AROZOS_BIN);
  41. }
  42. const builtBin = path.join(SRC_DIR, binaryName());
  43. if (fs.existsSync(builtBin)) {
  44. return builtBin;
  45. }
  46. console.log(" arozos binary not found, building it (go build)...");
  47. const res = spawnSync("go", ["build", "-o", binaryName(), "."], {
  48. cwd: SRC_DIR,
  49. stdio: "inherit"
  50. });
  51. if (res.status !== 0 || !fs.existsSync(builtBin)) {
  52. throw new Error("Failed to build the arozos binary. Install Go or set AROZOS_BIN.");
  53. }
  54. return builtBin;
  55. }
  56. // Wipe and recreate the isolated instance directory.
  57. function prepareInstanceDir() {
  58. fs.rmSync(INSTANCE_DIR, { recursive: true, force: true });
  59. fs.mkdirSync(INSTANCE_DIR, { recursive: true });
  60. fs.symlinkSync(WEB_ROOT, path.join(INSTANCE_DIR, "web"), "dir");
  61. fs.cpSync(SYSTEM_TEMPLATE, path.join(INSTANCE_DIR, "system"), { recursive: true });
  62. }
  63. function sleep(ms) {
  64. return new Promise(function (resolve) { setTimeout(resolve, ms); });
  65. }
  66. async function waitUntilReady(baseURL, timeoutMs) {
  67. const deadline = Date.now() + timeoutMs;
  68. let lastError = null;
  69. while (Date.now() < deadline) {
  70. try {
  71. const res = await fetch(baseURL + "/system/auth/checkLogin");
  72. if (res.ok) { return; }
  73. lastError = new Error("HTTP " + res.status);
  74. } catch (e) {
  75. lastError = e;
  76. }
  77. await sleep(250);
  78. }
  79. throw new Error("ArozOS server did not become ready in time: " + (lastError ? lastError.message : "unknown"));
  80. }
  81. // Create the first (admin) account on a freshly wiped instance.
  82. async function bootstrapAdmin(baseURL) {
  83. const res = await fetch(baseURL + "/system/auth/register", {
  84. method: "POST",
  85. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  86. body: new URLSearchParams({
  87. username: ADMIN_USER,
  88. password: ADMIN_PASS,
  89. group: "administrator"
  90. })
  91. });
  92. const text = (await res.text()).trim().toLowerCase();
  93. if (text.indexOf("ok") === -1) {
  94. throw new Error("Admin bootstrap failed: " + text);
  95. }
  96. }
  97. /*
  98. Start a disposable ArozOS server.
  99. Returns { baseURL, admin: {username, password}, stop() }.
  100. */
  101. async function start(options) {
  102. options = options || {};
  103. const port = options.port || Number(process.env.AROZ_PORT) || DEFAULT_PORT;
  104. const binary = resolveBinary();
  105. prepareInstanceDir();
  106. const logStream = fs.createWriteStream(path.join(INSTANCE_DIR, "server.log"));
  107. const args = [
  108. "-port", String(port),
  109. "-hostname", "E2E ArozOS",
  110. // Keep the test instance quiet and self-contained: no LAN discovery
  111. // broadcasts, no hardware/power hooks, no package auto-install and
  112. // no child subservice processes.
  113. "-allow_mdns=false",
  114. "-allow_ssdp=false",
  115. "-allow_upnp=false",
  116. "-allow_iot=false",
  117. "-disable_subservice",
  118. "-enable_hwman=false",
  119. "-enable_pwman=false",
  120. "-allow_pkg_install=false",
  121. "-enable_docker=false",
  122. "-arozcast_turn=false"
  123. ];
  124. const proc = spawn(binary, args, {
  125. cwd: INSTANCE_DIR,
  126. stdio: ["ignore", "pipe", "pipe"]
  127. });
  128. proc.stdout.pipe(logStream);
  129. proc.stderr.pipe(logStream);
  130. let exited = false;
  131. proc.on("exit", function () { exited = true; });
  132. const baseURL = "http://127.0.0.1:" + port;
  133. try {
  134. await waitUntilReady(baseURL, 120000);
  135. await bootstrapAdmin(baseURL);
  136. } catch (e) {
  137. proc.kill("SIGKILL");
  138. throw e;
  139. }
  140. function stop() {
  141. return new Promise(function (resolve) {
  142. if (exited) { return resolve(); }
  143. proc.on("exit", function () { resolve(); });
  144. proc.kill("SIGTERM");
  145. // Escalate if the server ignores SIGTERM.
  146. setTimeout(function () {
  147. if (!exited) { proc.kill("SIGKILL"); }
  148. }, 8000).unref();
  149. });
  150. }
  151. return {
  152. baseURL: baseURL,
  153. admin: { username: ADMIN_USER, password: ADMIN_PASS },
  154. instanceDir: INSTANCE_DIR,
  155. stop: stop
  156. };
  157. }
  158. module.exports = {
  159. start,
  160. ADMIN_USER,
  161. ADMIN_PASS,
  162. INSTANCE_DIR
  163. };