IKHSAN
ENID Baca dalam Bahasa Indonesia

homeio

Public

Forked from doctor-io/homeio

Branch
main
Latest commit
958d74616 October 2026

index.mjs

Open raw
#!/usr/bin/env node

import process from "node:process";
import net from "node:net";
import { parseRpcRequest, toErrorPayload } from "./protocol.mjs";
import { SOCKET_GROUP, SOCKET_PATH } from "./config.mjs";
import { log } from "./logger.mjs";
import { HelperServiceError, toCodeFromError } from "./errors.mjs";
import { ensureSocketDirectory, ensureSocketPermissions, safeUnlink } from "./socket-fs.mjs";
import { withTimeout } from "./async-utils.mjs";
import { createNetworkService, decodeSsid } from "./network-service.mjs";

const activeSockets = new Set();
let helperServer = null;

const networkService = createNetworkService({ log });
const {
  attachDbusEventBridge,
  connectNetwork,
  disconnectNetwork,
  getNetworkStatus,
  scanNetworks,
} = networkService;

function broadcastEvent(event) {
  const payload = `${JSON.stringify({
    type: "event",
    event,
  })}\n`;

  for (const socket of activeSockets) {
    if (!socket.writable) continue;
    socket.write(payload);
  }
}

async function handleRpcRequest(request) {
  if (request.method === "network.getStatus") {
    return withTimeout(getNetworkStatus());
  }

  if (request.method === "network.scan") {
    return withTimeout(scanNetworks(), 15_000);
  }

  if (request.method === "network.connect") {
    return withTimeout(connectNetwork(request.params, request.requestId), 20_000);
  }

  if (request.method === "network.disconnect") {
    return withTimeout(disconnectNetwork(request.params), 10_000);
  }

  throw new HelperServiceError("Method not allowed", {
    code: "invalid_request",
    statusCode: 400,
  });
}

async function start() {
  await ensureSocketDirectory(SOCKET_PATH, SOCKET_GROUP);
  await safeUnlink(SOCKET_PATH);

  helperServer = net.createServer((socket) => {
    activeSockets.add(socket);
    let buffer = "";

    socket.on("data", (chunk) => {
      buffer += chunk.toString("utf8");
      let newline = buffer.indexOf("\n");

      while (newline >= 0) {
        const line = buffer.slice(0, newline).trim();
        buffer = buffer.slice(newline + 1);
        newline = buffer.indexOf("\n");

        if (!line) continue;

        let parsedRequest = null;
        try {
          parsedRequest = parseRpcRequest(line);
        } catch (error) {
          const helperError = toCodeFromError(error);
          socket.write(
            `${JSON.stringify({
              id: null,
              ok: false,
              error: {
                code: helperError.code,
                message: helperError.message,
              },
            })}\n`,
          );
          continue;
        }

        const startedAt = performance.now();
        log({
          action: "dbus.network.rpc",
          status: "start",
          requestId: parsedRequest.requestId ?? parsedRequest.id,
          meta: {
            method: parsedRequest.method,
          },
        });

        void handleRpcRequest(parsedRequest)
          .then((result) => {
            socket.write(
              `${JSON.stringify({
                id: parsedRequest.id,
                ok: true,
                result,
              })}\n`,
            );

            log({
              action: "dbus.network.rpc",
              status: "success",
              requestId: parsedRequest.requestId ?? parsedRequest.id,
              durationMs: Number((performance.now() - startedAt).toFixed(2)),
              meta: {
                method: parsedRequest.method,
              },
            });
          })
          .catch((error) => {
            const helperError = toCodeFromError(error);
            socket.write(
              `${JSON.stringify({
                id: parsedRequest.id,
                ok: false,
                error: toErrorPayload(helperError),
              })}\n`,
            );

            log({
              level: helperError.statusCode >= 500 ? "error" : "warn",
              action: "dbus.network.rpc",
              status: "error",
              requestId: parsedRequest.requestId ?? parsedRequest.id,
              durationMs: Number((performance.now() - startedAt).toFixed(2)),
              meta: {
                method: parsedRequest.method,
              },
              error: helperError,
            });
          });
      }
    });

    socket.on("error", (error) => {
      log({
        level: "warn",
        action: "dbus.network.socket",
        status: "error",
        message: "Client socket error",
        error,
      });
    });

    socket.on("close", () => {
      activeSockets.delete(socket);
    });
  });

  await new Promise((resolve, reject) => {
    helperServer.once("error", reject);
    helperServer.listen(SOCKET_PATH, resolve);
  });

  await ensureSocketPermissions(SOCKET_PATH, SOCKET_GROUP);

  log({
    action: "dbus.network.helper.start",
    status: "success",
    meta: {
      socketPath: SOCKET_PATH,
      socketGroup: SOCKET_GROUP,
    },
  });

  attachDbusEventBridge(broadcastEvent).catch((error) => {
    const mapped = toCodeFromError(error);
    log({
      level: "warn",
      action: "dbus.network.events.start",
      status: "error",
      message: "DBus event bridge failed to start; helper will continue without events",
      error: mapped,
    });
  });
}

async function shutdown(signal) {
  log({
    action: "dbus.network.helper.stop",
    status: "start",
    message: `Received ${signal}, shutting down helper`,
  });

  if (helperServer) {
    await new Promise((resolve) => {
      helperServer.close(() => resolve(undefined));
    });
  }

  await safeUnlink(SOCKET_PATH);
  process.exit(0);
}

process.on("SIGINT", () => {
  void shutdown("SIGINT");
});

process.on("SIGTERM", () => {
  void shutdown("SIGTERM");
});

start().catch((error) => {
  const mapped = toCodeFromError(error);
  log({
    level: "error",
    action: "dbus.network.helper.start",
    status: "error",
    message: "Failed to start DBus helper",
    error: mapped,
  });
  process.exit(1);
});

export {
  HelperServiceError,
  connectNetwork,
  decodeSsid,
  disconnectNetwork,
  getNetworkStatus,
  scanNetworks,
  toCodeFromError,
};
bytes
6,115
lines
241
Type
mjs
Latest commit
16 October 2026