@the-seeker/server-agent 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,19 @@ capability: `available`=조회 성공, `unsupported`=OS 미지원 또는 nginx
84
84
  `not_installed`=실행 파일 없음, `permission_denied`=권한 부족, `error`=기타 조회/파싱 실패.
85
85
  호스트 조회 실패는 호출자가 처리하고 host capability를 `error`로 지정합니다.
86
86
 
87
+ ## 문제 해결
88
+
89
+ systemd의 PATH에서 nvm으로 설치한 PM2가 보이지 않으면 capability가 `not_installed`가 될 수 있습니다. `@the-seeker/server-agent@0.1.1`부터는 실행 중인 node의 `dirname(process.execPath)/pm2`를 자동으로 찾고, `install`이 systemd unit에 `Environment=PATH=<node bin dir>:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`을 기록합니다. 업그레이드 후 호스트에서 `install`을 다시 실행하면 됩니다(0.1.1은 이어서 `sudo systemctl restart theseeker-agent` 필요, 0.1.2부터 `install`이 자동 재시작).
90
+
91
+ 자동 설정을 쓰지 않을 때는 systemd drop-in으로 PM2 실행 파일을 지정합니다.
92
+
93
+ ```ini
94
+ [Service]
95
+ Environment=PM2_BIN=/home/<user>/.nvm/versions/node/<ver>/bin/pm2
96
+ ```
97
+
98
+ drop-in 저장 후 `sudo systemctl restart theseeker-agent`를 실행합니다.
99
+
87
100
  ## 상태 확인과 제거
88
101
 
89
102
  Ubuntu:
@@ -4,7 +4,12 @@ export declare function run(file: string, args: readonly string[], options?: Omi
4
4
  stdout: string;
5
5
  stderr: string;
6
6
  }>;
7
- export declare function locate(binary: string): Promise<string | null>;
7
+ export interface LocateDeps {
8
+ readonly execPath?: string;
9
+ readonly access?: (path: string, mode: number) => Promise<void>;
10
+ }
11
+ /** systemd units run with a minimal PATH, so a nvm-installed `pm2` is invisible to `which` but sits next to the node binary in ExecStart. */
12
+ export declare function locate(binary: string, deps?: LocateDeps): Promise<string | null>;
8
13
  export declare class CollectionError extends Error {
9
14
  readonly code: ResponseError["code"];
10
15
  constructor(code: ResponseError["code"]);
@@ -1,4 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { access, constants } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
2
4
  export function run(file, args, options = {}) {
3
5
  return new Promise((resolve, reject) => {
4
6
  execFile(file, [...args], { timeout: 8000, maxBuffer: 8 * 1024 * 1024, ...options, shell: false, encoding: "utf8" }, (error, stdout, stderr) => {
@@ -9,15 +11,25 @@ export function run(file, args, options = {}) {
9
11
  });
10
12
  });
11
13
  }
12
- export async function locate(binary) {
14
+ /** systemd units run with a minimal PATH, so a nvm-installed `pm2` is invisible to `which` but sits next to the node binary in ExecStart. */
15
+ export async function locate(binary, deps = {}) {
13
16
  try {
14
- const { stdout } = await run("which", [binary]);
15
- return stdout.trim() || null;
17
+ const found = (await run("which", [binary])).stdout.trim();
18
+ if (found)
19
+ return found;
16
20
  }
17
21
  catch (error) {
18
- if (error instanceof Error && "code" in error && (error.code === 1 || error.code === "ENOENT"))
19
- return null;
20
- throw error;
22
+ if (!(error instanceof Error && "code" in error && (error.code === 1 || error.code === "ENOENT")))
23
+ throw error;
24
+ }
25
+ const candidate = join(dirname(deps.execPath ?? process.execPath), binary);
26
+ const isExecutable = deps.access ?? access;
27
+ try {
28
+ await isExecutable(candidate, constants.X_OK);
29
+ return candidate;
30
+ }
31
+ catch {
32
+ return null;
21
33
  }
22
34
  }
23
35
  export class CollectionError extends Error {
@@ -3,6 +3,8 @@ export declare const SERVICE_NAME = "theseeker-agent";
3
3
  export declare const UNIT_PATH = "/etc/systemd/system/theseeker-agent.service";
4
4
  export declare const UNIT_FILE_MODE = 420;
5
5
  export declare const SUPPLEMENTARY_GROUPS: readonly ["adm", "systemd-journal"];
6
+ /** systemd gives a unit no PATH at all, so the node bin directory (nvm keeps `pm2` there) is prepended to this system default. */
7
+ export declare const SYSTEM_PATH_TAIL = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
6
8
  export interface UnitOptions {
7
9
  readonly user: string;
8
10
  readonly group: string;
@@ -1,9 +1,12 @@
1
+ import { dirname } from "node:path";
1
2
  import { SYSTEM_CONFIG_DIR } from "../config.js";
2
3
  import { skipsServiceManager } from "./detect.js";
3
4
  export const SERVICE_NAME = "theseeker-agent";
4
5
  export const UNIT_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
5
6
  export const UNIT_FILE_MODE = 0o644;
6
7
  export const SUPPLEMENTARY_GROUPS = ["adm", "systemd-journal"];
8
+ /** systemd gives a unit no PATH at all, so the node bin directory (nvm keeps `pm2` there) is prepended to this system default. */
9
+ export const SYSTEM_PATH_TAIL = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
7
10
  /** `ProtectSystem=strict` still hides `/home`; the agent reads PM2 logs there, so a `/home` PM2_HOME disables the guard. */
8
11
  export function protectHomeValue(pm2Home) {
9
12
  return pm2Home.startsWith("/home/") || pm2Home === "/home" ? "false" : "read-only";
@@ -23,7 +26,7 @@ export function buildUnitFile(options) {
23
26
  ];
24
27
  if (options.addGroups)
25
28
  lines.push(`SupplementaryGroups=${SUPPLEMENTARY_GROUPS.join(" ")}`);
26
- lines.push(`Environment=PM2_HOME=${options.pm2Home}`, `ExecStart=${options.nodePath} ${options.binPath} run`, "Restart=always", "RestartSec=5", "NoNewPrivileges=true", "ProtectSystem=strict", `ProtectHome=${protectHomeValue(options.pm2Home)}`, `ReadWritePaths=${SYSTEM_CONFIG_DIR}`, "", "[Install]", "WantedBy=multi-user.target", "");
29
+ lines.push(`Environment=PATH=${dirname(options.nodePath)}:${SYSTEM_PATH_TAIL}`, `Environment=PM2_HOME=${options.pm2Home}`, `ExecStart=${options.nodePath} ${options.binPath} run`, "Restart=always", "RestartSec=5", "NoNewPrivileges=true", "ProtectSystem=strict", `ProtectHome=${protectHomeValue(options.pm2Home)}`, `ReadWritePaths=${SYSTEM_CONFIG_DIR}`, "", "[Install]", "WantedBy=multi-user.target", "");
27
30
  return lines.join("\n");
28
31
  }
29
32
  export function unitOptionsFrom(detection, addGroups) {
@@ -51,6 +54,7 @@ export function manualInstallCommands(endpoint, user, addGroups) {
51
54
  "and then runs:",
52
55
  " sudo systemctl daemon-reload",
53
56
  ` sudo systemctl enable --now ${SERVICE_NAME}`,
57
+ ` sudo systemctl restart ${SERVICE_NAME}`,
54
58
  ];
55
59
  }
56
60
  export function manualGroupCommand(user) {
@@ -86,6 +90,11 @@ export async function installSystemd(environment, options) {
86
90
  if (!(await runSystemctl(environment, ["enable", "--now", SERVICE_NAME])))
87
91
  return 1;
88
92
  environment.print(`service enabled: ${SERVICE_NAME}`);
93
+ // `enable --now` no-ops on an already-running service, so an upgraded package or a rewritten unit/config
94
+ // would keep running the old process until someone restarted it by hand.
95
+ if (!(await runSystemctl(environment, ["restart", SERVICE_NAME])))
96
+ return 1;
97
+ environment.print(`service restarted: ${SERVICE_NAME}`);
89
98
  if (!options.addGroups)
90
99
  for (const line of manualGroupCommand(options.detection.user))
91
100
  environment.print(line);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-seeker/server-agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "bin": { "theseeker-agent": "dist/cli.js" },
6
6
  "engines": { "node": ">=20" },