matter-server 0.5.4 → 0.5.5

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.
@@ -13,31 +13,15 @@ import {
13
13
  MatterController,
14
14
  WebSocketControllerHandler
15
15
  } from "@matter-server/ws-controller";
16
- import { open } from "node:fs/promises";
17
16
  import { getCliOptions } from "./cli.js";
18
17
  import { LegacyDataWriter, loadLegacyData } from "./converter/index.js";
18
+ import { createFileLogger } from "./file-logger.js";
19
19
  import { initializeOta } from "./ota.js";
20
20
  import { HealthHandler } from "./server/HealthHandler.js";
21
21
  import { StaticFileHandler } from "./server/StaticFileHandler.js";
22
22
  import { WebServer } from "./server/WebServer.js";
23
23
  import { MATTER_SERVER_VERSION } from "./version.js";
24
24
  import "@matter-server/custom-clusters";
25
- async function createFileLogger(path) {
26
- const fileHandle = await open(path, "a");
27
- const writer = fileHandle.createWriteStream();
28
- process.on(
29
- "beforeExit",
30
- () => void fileHandle.close().catch((err) => err && console.error(`Failed to close log file: ${err}`))
31
- );
32
- return (formattedLog) => {
33
- try {
34
- writer.write(`${formattedLog}
35
- `);
36
- } catch (error) {
37
- console.error(`Failed to write to log file: ${error}`);
38
- }
39
- };
40
- }
41
25
  const cliOptions = getCliOptions();
42
26
  function mapLogLevel(level) {
43
27
  switch (level) {
@@ -74,12 +58,14 @@ let server;
74
58
  let config;
75
59
  let legacyData;
76
60
  let legacyDataWriter;
61
+ let fileLoggerClose;
77
62
  async function start() {
78
63
  if (cliOptions.logFile) {
79
64
  try {
80
- const fileWriter = await createFileLogger(cliOptions.logFile);
65
+ const fileLogger = await createFileLogger(cliOptions.logFile);
66
+ fileLoggerClose = fileLogger.close;
81
67
  Logger.destinations.file = LogDestination({
82
- write: fileWriter,
68
+ write: fileLogger.write,
83
69
  level: mapLogLevel(cliOptions.logLevel),
84
70
  format: LogFormat("plain")
85
71
  });
@@ -135,7 +121,7 @@ async function start() {
135
121
  legacyServerData
136
122
  );
137
123
  if (!cliOptions.disableOta) {
138
- await initializeOta(controller, cliOptions);
124
+ controller.commandHandler?.events.started.once(async () => await initializeOta(controller, cliOptions));
139
125
  }
140
126
  if (legacyData.serverFile && legacyData.fabricConfig) {
141
127
  legacyDataWriter = new LegacyDataWriter(env, cliOptions.storagePath, legacyData.fabricConfig);
@@ -155,6 +141,11 @@ async function start() {
155
141
  logger.info("Dashboard disabled via CLI flag");
156
142
  }
157
143
  server = new WebServer({ listenAddresses: cliOptions.listenAddress, port: cliOptions.port }, handlers);
144
+ if (!cliOptions.listenAddress) {
145
+ logger.warn(
146
+ `WebSocket server is listening on all network interfaces. Use --listen-address to restrict access. Ensure your environment (firewall, network) prevents unauthorized access.`
147
+ );
148
+ }
158
149
  await server.start();
159
150
  }
160
151
  async function stop() {
@@ -165,6 +156,11 @@ async function stop() {
165
156
  await legacyDataWriter.flush();
166
157
  }
167
158
  await config?.close();
159
+ try {
160
+ await fileLoggerClose?.();
161
+ } catch (err) {
162
+ console.error(`Failed to flush log file on shutdown: ${err}`);
163
+ }
168
164
  process.exit(0);
169
165
  }
170
166
  start().catch((err) => console.error(err));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/MatterServer.ts"],
4
- "mappings": "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",
4
+ "mappings": "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",
5
5
  "names": []
6
6
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Creates a file-based logger that writes to the given path.
8
+ * On startup and every 24 hours the existing backups are shifted
9
+ * (.6→.7, .5→.6, …, .1→.2, current→.1) and a fresh file is opened,
10
+ * keeping up to LOG_BACKUP_COUNT daily backups (≈7 days of logs).
11
+ *
12
+ * Returns `{ write, close }`. Call `close()` before `process.exit()` to flush
13
+ * any buffered data and release the file handle.
14
+ */
15
+ export declare function createFileLogger(logPath: string): Promise<{
16
+ write: (formattedLog: string) => void;
17
+ close: () => Promise<void>;
18
+ }>;
19
+ //# sourceMappingURL=file-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-logger.d.ts","sourceRoot":"","sources":["../../src/file-logger.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM;0BAgMxB,MAAM;;GAWnC"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { closeSync, createWriteStream, renameSync, unlinkSync } from "node:fs";
7
+ import { mkdir } from "node:fs/promises";
8
+ import { basename, dirname, posix, sep } from "node:path";
9
+ const LOG_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
10
+ const LOG_BACKUP_COUNT = 7;
11
+ const LOG_TEMP_SUFFIX = ".new";
12
+ function isEnoent(err) {
13
+ return err instanceof Error && err.code === "ENOENT";
14
+ }
15
+ async function createFileLogger(logPath) {
16
+ if (logPath.endsWith(sep) || logPath.endsWith(posix.sep) || !basename(logPath)) {
17
+ throw new Error(`Log file path must include a filename, not just a directory: "${logPath}"`);
18
+ }
19
+ await mkdir(dirname(logPath), { recursive: true });
20
+ function shiftBackupsSync() {
21
+ try {
22
+ unlinkSync(`${logPath}.${LOG_BACKUP_COUNT}`);
23
+ } catch (err) {
24
+ if (!isEnoent(err)) throw err;
25
+ }
26
+ for (let i = LOG_BACKUP_COUNT - 1; i >= 1; i--) {
27
+ try {
28
+ renameSync(`${logPath}.${i}`, `${logPath}.${i + 1}`);
29
+ } catch (err) {
30
+ if (!isEnoent(err)) throw err;
31
+ }
32
+ }
33
+ try {
34
+ renameSync(logPath, `${logPath}.1`);
35
+ } catch (err) {
36
+ if (!isEnoent(err)) throw err;
37
+ }
38
+ }
39
+ function openLogStream(filePath, flags = "a", autoClose = true) {
40
+ return new Promise((resolve, reject) => {
41
+ const stream = createWriteStream(filePath, { flags, autoClose });
42
+ stream.once("open", (fd) => resolve({ stream, fd }));
43
+ stream.once("error", reject);
44
+ });
45
+ }
46
+ function drainStream(stream) {
47
+ return new Promise((resolve, reject) => {
48
+ const onError = (err) => reject(err);
49
+ stream.once("error", onError);
50
+ stream.end((err) => {
51
+ stream.removeListener("error", onError);
52
+ if (err) reject(err);
53
+ else resolve();
54
+ });
55
+ });
56
+ }
57
+ try {
58
+ unlinkSync(`${logPath}${LOG_TEMP_SUFFIX}`);
59
+ } catch (err) {
60
+ if (!isEnoent(err)) console.error(`Failed to clean up stale log temp file: ${err}`);
61
+ }
62
+ try {
63
+ shiftBackupsSync();
64
+ } catch (err) {
65
+ console.error(`Initial log file backup shifting failed: ${err}`);
66
+ }
67
+ let { stream: writer } = await openLogStream(logPath);
68
+ writer.on("error", (err) => console.error(`Log file write error: ${err}`));
69
+ let pendingLines = null;
70
+ let rotationPromise = null;
71
+ async function doRotate() {
72
+ const tempPath = `${logPath}${LOG_TEMP_SUFFIX}`;
73
+ let tempStream;
74
+ let tempFd;
75
+ try {
76
+ ({ stream: tempStream, fd: tempFd } = await openLogStream(tempPath, "w", false));
77
+ tempStream.on("error", (err) => console.error(`Log file write error: ${err}`));
78
+ } catch (err) {
79
+ console.error(`Failed to open temp log file for rotation: ${err}`);
80
+ return;
81
+ }
82
+ const oldStream = writer;
83
+ writer = tempStream;
84
+ try {
85
+ await drainStream(oldStream);
86
+ } catch (err) {
87
+ console.error(`Failed to flush old log file during rotation: ${err}`);
88
+ }
89
+ try {
90
+ shiftBackupsSync();
91
+ } catch (err) {
92
+ console.error(`Log file backup shifting failed: ${err}`);
93
+ }
94
+ pendingLines = [];
95
+ try {
96
+ await drainStream(tempStream);
97
+ } catch (err) {
98
+ console.error(`Failed to flush temp log file during rotation: ${err}`);
99
+ }
100
+ try {
101
+ closeSync(tempFd);
102
+ renameSync(tempPath, logPath);
103
+ } catch (err) {
104
+ console.error(`Failed to finalize log file rotation: ${err}`);
105
+ }
106
+ let finalStream;
107
+ try {
108
+ ({ stream: finalStream } = await openLogStream(logPath, "a"));
109
+ finalStream.on("error", (err) => console.error(`Log file write error: ${err}`));
110
+ } catch (err) {
111
+ console.error(`Failed to open final log file after rotation: ${err}`);
112
+ }
113
+ const buffered = pendingLines ?? [];
114
+ pendingLines = null;
115
+ if (finalStream !== void 0) {
116
+ writer = finalStream;
117
+ for (const line of buffered) {
118
+ finalStream.write(`${line}
119
+ `);
120
+ }
121
+ }
122
+ }
123
+ function rotateLog() {
124
+ if (rotationPromise !== null) {
125
+ return;
126
+ }
127
+ rotationPromise = doRotate().catch((err) => console.error(`Log file rotation failed: ${err}`)).finally(() => {
128
+ rotationPromise = null;
129
+ });
130
+ }
131
+ const rotationTimer = setInterval(() => rotateLog(), LOG_ROTATION_INTERVAL_MS);
132
+ rotationTimer.unref();
133
+ async function close() {
134
+ clearInterval(rotationTimer);
135
+ if (rotationPromise !== null) {
136
+ await rotationPromise.catch(() => {
137
+ });
138
+ }
139
+ await drainStream(writer).catch((err) => console.error(`Failed to flush log file on close: ${err}`));
140
+ }
141
+ return {
142
+ write: (formattedLog) => {
143
+ if (pendingLines !== null) {
144
+ pendingLines.push(formattedLog);
145
+ } else if (!writer.writableEnded && !writer.destroyed) {
146
+ writer.write(`${formattedLog}
147
+ `);
148
+ }
149
+ },
150
+ close
151
+ };
152
+ }
153
+ export {
154
+ createFileLogger
155
+ };
156
+ //# sourceMappingURL=file-logger.js.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/file-logger.ts"],
4
+ "mappings": "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",
5
+ "names": []
6
+ }
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
9
9
  function getMatterServerVersion() {
10
10
  const thisFile = fileURLToPath(import.meta.url);
11
11
  let dir = dirname(thisFile);
12
- while (dir !== "/") {
12
+ while (dirname(dir) !== dir) {
13
13
  try {
14
14
  const packageJsonPath = join(dir, "package.json");
15
15
  const content = readFileSync(packageJsonPath, "utf-8");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/version.ts"],
4
- "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAU9B,SAAS,yBAAiC;AAEtC,QAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,MAAI,MAAM,QAAQ,QAAQ;AAG1B,SAAO,QAAQ,KAAK;AAChB,QAAI;AACA,YAAM,kBAAkB,KAAK,KAAK,cAAc;AAChD,YAAM,UAAU,aAAa,iBAAiB,OAAO;AACrD,YAAM,MAAM,KAAK,MAAM,OAAO;AAC9B,UAAI,IAAI,SAAS,iBAAiB;AAC9B,eAAO,IAAI;AAAA,MACf;AAAA,IACJ,QAAQ;AAAA,IAER;AACA,UAAM,QAAQ,GAAG;AAAA,EACrB;AACA,QAAM,IAAI,MAAM,2CAA2C;AAC/D;AAGO,MAAM,wBAAwB,uBAAuB;",
4
+ "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAU9B,SAAS,yBAAiC;AAEtC,QAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,MAAI,MAAM,QAAQ,QAAQ;AAG1B,SAAO,QAAQ,GAAG,MAAM,KAAK;AACzB,QAAI;AACA,YAAM,kBAAkB,KAAK,KAAK,cAAc;AAChD,YAAM,UAAU,aAAa,iBAAiB,OAAO;AACrD,YAAM,MAAM,KAAK,MAAM,OAAO;AAC9B,UAAI,IAAI,SAAS,iBAAiB;AAC9B,eAAO,IAAI;AAAA,MACf;AAAA,IACJ,QAAQ;AAAA,IAER;AACA,UAAM,QAAQ,GAAG;AAAA,EACrB;AACA,QAAM,IAAI,MAAM,2CAA2C;AAC/D;AAGO,MAAM,wBAAwB,uBAAuB;",
5
5
  "names": []
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matter-server",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "description": "WebSocket Matter server based on matter.js",
6
6
  "bugs": {
@@ -23,19 +23,19 @@
23
23
  "node": ">=20.19.0 <22.0.0 || >=22.13.0"
24
24
  },
25
25
  "dependencies": {
26
- "@matter/main": "0.17.0-alpha.0-20260308-2d3759f3b",
27
- "@matter-server/ws-controller": "0.5.4",
28
- "@matter-server/dashboard": "0.5.4",
29
- "@matter-server/custom-clusters": "0.5.4",
26
+ "@matter/main": "0.17.0-alpha.0-20260311-5fb2f183e",
27
+ "@matter-server/ws-controller": "0.5.5",
28
+ "@matter-server/dashboard": "0.5.5",
29
+ "@matter-server/custom-clusters": "0.5.5",
30
30
  "commander": "^14.0.3",
31
31
  "express": "^5.2.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/express": "^5.0.6",
35
- "@types/node": "^25.3.3",
36
- "@matter/nodejs": "0.17.0-alpha.0-20260308-2d3759f3b",
37
- "@matter/testing": "0.17.0-alpha.0-20260308-2d3759f3b",
38
- "@matter-server/ws-client": "0.5.4"
35
+ "@types/node": "^25.4.0",
36
+ "@matter/nodejs": "0.17.0-alpha.0-20260311-5fb2f183e",
37
+ "@matter/testing": "0.17.0-alpha.0-20260311-5fb2f183e",
38
+ "@matter-server/ws-client": "0.5.5"
39
39
  },
40
40
  "files": [
41
41
  "dist/**/*",
@@ -16,9 +16,9 @@ import {
16
16
  WebServerHandler,
17
17
  WebSocketControllerHandler,
18
18
  } from "@matter-server/ws-controller";
19
- import { open } from "node:fs/promises";
20
19
  import { getCliOptions, type LogLevel as CliLogLevel } from "./cli.js";
21
20
  import { LegacyDataWriter, loadLegacyData, type LegacyData } from "./converter/index.js";
21
+ import { createFileLogger } from "./file-logger.js";
22
22
  import { initializeOta } from "./ota.js";
23
23
  import { HealthHandler } from "./server/HealthHandler.js";
24
24
  import { StaticFileHandler } from "./server/StaticFileHandler.js";
@@ -27,27 +27,6 @@ import { MATTER_SERVER_VERSION } from "./version.js";
27
27
  // Register the custom clusters
28
28
  import "@matter-server/custom-clusters";
29
29
 
30
- /**
31
- * Creates a file-based logger that appends to the given path.
32
- * The file is opened on start and closed when the process shuts down.
33
- */
34
- async function createFileLogger(path: string) {
35
- const fileHandle = await open(path, "a");
36
- const writer = fileHandle.createWriteStream();
37
- process.on(
38
- "beforeExit",
39
- () => void fileHandle.close().catch(err => err && console.error(`Failed to close log file: ${err}`)),
40
- );
41
-
42
- return (formattedLog: string) => {
43
- try {
44
- writer.write(`${formattedLog}\n`);
45
- } catch (error) {
46
- console.error(`Failed to write to log file: ${error}`);
47
- }
48
- };
49
- }
50
-
51
30
  // Parse CLI options early for logging setup
52
31
  const cliOptions = getCliOptions();
53
32
 
@@ -98,14 +77,16 @@ let server: WebServer;
98
77
  let config: ConfigStorage;
99
78
  let legacyData: LegacyData;
100
79
  let legacyDataWriter: LegacyDataWriter | undefined;
80
+ let fileLoggerClose: (() => Promise<void>) | undefined;
101
81
 
102
82
  async function start() {
103
83
  // Set up file logging additionally to the console if configured
104
84
  if (cliOptions.logFile) {
105
85
  try {
106
- const fileWriter = await createFileLogger(cliOptions.logFile);
86
+ const fileLogger = await createFileLogger(cliOptions.logFile);
87
+ fileLoggerClose = fileLogger.close;
107
88
  Logger.destinations.file = LogDestination({
108
- write: fileWriter,
89
+ write: fileLogger.write,
109
90
  level: mapLogLevel(cliOptions.logLevel),
110
91
  format: LogFormat("plain"),
111
92
  });
@@ -173,7 +154,7 @@ async function start() {
173
154
  );
174
155
 
175
156
  if (!cliOptions.disableOta) {
176
- await initializeOta(controller, cliOptions);
157
+ controller.commandHandler?.events.started.once(async () => await initializeOta(controller, cliOptions));
177
158
  }
178
159
 
179
160
  // Subscribe to node events for legacy data file updates
@@ -199,6 +180,12 @@ async function start() {
199
180
  }
200
181
  server = new WebServer({ listenAddresses: cliOptions.listenAddress, port: cliOptions.port }, handlers);
201
182
 
183
+ if (!cliOptions.listenAddress) {
184
+ logger.warn(
185
+ `WebSocket server is listening on all network interfaces. Use --listen-address to restrict access. Ensure your environment (firewall, network) prevents unauthorized access.`,
186
+ );
187
+ }
188
+
202
189
  await server.start();
203
190
  }
204
191
 
@@ -211,6 +198,11 @@ async function stop() {
211
198
  await legacyDataWriter.flush();
212
199
  }
213
200
  await config?.close();
201
+ try {
202
+ await fileLoggerClose?.();
203
+ } catch (err) {
204
+ console.error(`Failed to flush log file on shutdown: ${err}`);
205
+ }
214
206
  process.exit(0);
215
207
  }
216
208
 
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { closeSync, createWriteStream, renameSync, unlinkSync, type WriteStream } from "node:fs";
8
+ import { mkdir } from "node:fs/promises";
9
+ import { basename, dirname, posix, sep } from "node:path";
10
+
11
+ const LOG_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
12
+ const LOG_BACKUP_COUNT = 7; // keep up to 7 daily backup files
13
+ const LOG_TEMP_SUFFIX = ".new"; // temp file written to during rotation
14
+
15
+ function isEnoent(err: unknown): boolean {
16
+ return err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT";
17
+ }
18
+
19
+ /**
20
+ * Creates a file-based logger that writes to the given path.
21
+ * On startup and every 24 hours the existing backups are shifted
22
+ * (.6→.7, .5→.6, …, .1→.2, current→.1) and a fresh file is opened,
23
+ * keeping up to LOG_BACKUP_COUNT daily backups (≈7 days of logs).
24
+ *
25
+ * Returns `{ write, close }`. Call `close()` before `process.exit()` to flush
26
+ * any buffered data and release the file handle.
27
+ */
28
+ export async function createFileLogger(logPath: string) {
29
+ // sep is "\\" on Windows, "/" on POSIX; posix.sep is always "/"
30
+ if (logPath.endsWith(sep) || logPath.endsWith(posix.sep) || !basename(logPath)) {
31
+ throw new Error(`Log file path must include a filename, not just a directory: "${logPath}"`);
32
+ }
33
+ await mkdir(dirname(logPath), { recursive: true });
34
+
35
+ /** Shift backups synchronously: .6→.7, …, .1→.2, logPath→.1 */
36
+ function shiftBackupsSync() {
37
+ try {
38
+ unlinkSync(`${logPath}.${LOG_BACKUP_COUNT}`);
39
+ } catch (err) {
40
+ if (!isEnoent(err)) throw err;
41
+ }
42
+ for (let i = LOG_BACKUP_COUNT - 1; i >= 1; i--) {
43
+ try {
44
+ renameSync(`${logPath}.${i}`, `${logPath}.${i + 1}`);
45
+ } catch (err) {
46
+ if (!isEnoent(err)) throw err;
47
+ }
48
+ }
49
+ try {
50
+ renameSync(logPath, `${logPath}.1`);
51
+ } catch (err) {
52
+ if (!isEnoent(err)) throw err;
53
+ }
54
+ }
55
+
56
+ /** Open a write stream at filePath and wait until it is ready.
57
+ * Pass autoClose:false for streams whose fd lifetime is managed explicitly
58
+ * (e.g. the rotation temp stream, where we call closeSync before renaming). */
59
+ function openLogStream(
60
+ filePath: string,
61
+ flags = "a",
62
+ autoClose = true,
63
+ ): Promise<{ stream: WriteStream; fd: number }> {
64
+ return new Promise<{ stream: WriteStream; fd: number }>((resolve, reject) => {
65
+ const stream = createWriteStream(filePath, { flags, autoClose });
66
+ stream.once("open", (fd: number) => resolve({ stream, fd }));
67
+ stream.once("error", reject);
68
+ });
69
+ }
70
+
71
+ /** End a stream and resolve when all data is flushed ('finish').
72
+ * Rejects if the stream emits an error before finishing, preventing an
73
+ * unresolvable hang when e.g. the disk is full during a rotation flush. */
74
+ function drainStream(stream: WriteStream): Promise<void> {
75
+ return new Promise<void>((resolve, reject) => {
76
+ const onError = (err: Error) => reject(err);
77
+ stream.once("error", onError);
78
+ stream.end((err?: Error | null) => {
79
+ stream.removeListener("error", onError);
80
+ if (err) reject(err);
81
+ else resolve();
82
+ });
83
+ });
84
+ }
85
+
86
+ // Startup: clean up any stale temp file from a previously crashed rotation,
87
+ // shift existing backups, then open the log file.
88
+ try {
89
+ unlinkSync(`${logPath}${LOG_TEMP_SUFFIX}`);
90
+ } catch (err) {
91
+ if (!isEnoent(err)) console.error(`Failed to clean up stale log temp file: ${err}`);
92
+ }
93
+ try {
94
+ shiftBackupsSync();
95
+ } catch (err) {
96
+ console.error(`Initial log file backup shifting failed: ${err}`);
97
+ }
98
+ let { stream: writer } = await openLogStream(logPath);
99
+ writer.on("error", err => console.error(`Log file write error: ${err}`));
100
+
101
+ // When non-null, write() buffers lines here instead of writing to the stream.
102
+ // Used during the drain+rename phase of rotation to capture writes that arrive
103
+ // while tempStream is being flushed, with no fd open for them yet.
104
+ let pendingLines: string[] | null = null;
105
+
106
+ // Track in-progress rotation so close() can wait for it to finish.
107
+ let rotationPromise: Promise<void> | null = null;
108
+
109
+ async function doRotate() {
110
+ const tempPath = `${logPath}${LOG_TEMP_SUFFIX}`;
111
+
112
+ // 1. Open the temp file with "w" (truncate) — ensures any stale .new file from
113
+ // a previously crashed rotation is discarded rather than appended to.
114
+ let tempStream: WriteStream;
115
+ let tempFd: number;
116
+ try {
117
+ // autoClose:false — we own tempFd and must closeSync it before renameSync (required on Windows)
118
+ ({ stream: tempStream, fd: tempFd } = await openLogStream(tempPath, "w", false));
119
+ tempStream.on("error", err => console.error(`Log file write error: ${err}`));
120
+ } catch (err) {
121
+ console.error(`Failed to open temp log file for rotation: ${err}`);
122
+ return;
123
+ }
124
+
125
+ // 2. Atomic swap — single-threaded JS, so no write() can execute between
126
+ // this assignment and any following synchronous statement.
127
+ const oldStream = writer;
128
+ writer = tempStream;
129
+
130
+ // 3. Drain the old stream. New writes are already going to tempStream.
131
+ try {
132
+ await drainStream(oldStream);
133
+ } catch (err) {
134
+ console.error(`Failed to flush old log file during rotation: ${err}`);
135
+ }
136
+
137
+ // 4. Shift the backups now that the old file handle is drained and closed.
138
+ try {
139
+ shiftBackupsSync();
140
+ } catch (err) {
141
+ // Backups could not be shifted; log continues at tempPath with the wrong
142
+ // name. Still attempt the rename so the active file is at logPath.
143
+ console.error(`Log file backup shifting failed: ${err}`);
144
+ }
145
+
146
+ // 5. Enable in-memory buffering so writes that arrive during the next two
147
+ // async steps are captured rather than going to the soon-to-be-closed
148
+ // tempStream or a not-yet-open finalStream.
149
+ pendingLines = [];
150
+
151
+ // 6. Drain tempStream fully before closing its fd. This guarantees no
152
+ // in-flight libuv fs.write() calls remain when closeSync runs below,
153
+ // eliminating the risk of EBADF errors from a closed fd.
154
+ try {
155
+ await drainStream(tempStream);
156
+ } catch (err) {
157
+ console.error(`Failed to flush temp log file during rotation: ${err}`);
158
+ }
159
+
160
+ // 7. Close tempFd (required on Windows before rename) and rename .new → logPath.
161
+ // pendingLines buffers any writes that arrive while this is in progress.
162
+ try {
163
+ closeSync(tempFd);
164
+ renameSync(tempPath, logPath);
165
+ } catch (err) {
166
+ console.error(`Failed to finalize log file rotation: ${err}`);
167
+ // Rotation failed: reopen logPath in append mode so logging can continue.
168
+ // pendingLines is flushed into the reopened stream below.
169
+ }
170
+
171
+ // 8. Open the final stream. pendingLines remains active so that writes
172
+ // arriving while the file is opening are buffered, not dropped.
173
+ let finalStream: WriteStream | undefined;
174
+ try {
175
+ ({ stream: finalStream } = await openLogStream(logPath, "a"));
176
+ finalStream.on("error", err => console.error(`Log file write error: ${err}`));
177
+ } catch (err) {
178
+ console.error(`Failed to open final log file after rotation: ${err}`);
179
+ // writer still points to the ended tempStream; the guards in write()
180
+ // (writableEnded / destroyed) will silently drop further writes.
181
+ }
182
+
183
+ // Disable buffering and flush everything captured since step 5. Done
184
+ // synchronously so write() cannot observe a window where pendingLines is
185
+ // null but writer still points to the ended tempStream.
186
+ const buffered = pendingLines ?? [];
187
+ pendingLines = null;
188
+ if (finalStream !== undefined) {
189
+ writer = finalStream;
190
+ for (const line of buffered) {
191
+ finalStream.write(`${line}\n`);
192
+ }
193
+ }
194
+ }
195
+
196
+ function rotateLog() {
197
+ if (rotationPromise !== null) {
198
+ return;
199
+ }
200
+ rotationPromise = doRotate()
201
+ .catch(err => console.error(`Log file rotation failed: ${err}`))
202
+ .finally(() => {
203
+ rotationPromise = null;
204
+ });
205
+ }
206
+
207
+ const rotationTimer = setInterval(() => rotateLog(), LOG_ROTATION_INTERVAL_MS);
208
+ rotationTimer.unref();
209
+
210
+ async function close() {
211
+ clearInterval(rotationTimer);
212
+ // If a rotation is in progress, let it finish before we close.
213
+ if (rotationPromise !== null) {
214
+ await rotationPromise.catch(() => {});
215
+ }
216
+ await drainStream(writer).catch(err => console.error(`Failed to flush log file on close: ${err}`));
217
+ }
218
+
219
+ return {
220
+ write: (formattedLog: string) => {
221
+ if (pendingLines !== null) {
222
+ // Rotation in progress: buffer the line; it will be flushed to the
223
+ // final stream once the new file is open (step 8 in doRotate).
224
+ pendingLines.push(formattedLog);
225
+ } else if (!writer.writableEnded && !writer.destroyed) {
226
+ writer.write(`${formattedLog}\n`);
227
+ }
228
+ },
229
+ close,
230
+ };
231
+ }
package/src/version.ts CHANGED
@@ -25,8 +25,8 @@ function getMatterServerVersion(): string {
25
25
  const thisFile = fileURLToPath(import.meta.url);
26
26
  let dir = dirname(thisFile);
27
27
 
28
- // Navigate up to find package.json
29
- while (dir !== "/") {
28
+ // Navigate up to find package.json; stop at filesystem root (portable across OS)
29
+ while (dirname(dir) !== dir) {
30
30
  try {
31
31
  const packageJsonPath = join(dir, "package.json");
32
32
  const content = readFileSync(packageJsonPath, "utf-8");