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.
- package/dist/esm/MatterServer.js +16 -20
- package/dist/esm/MatterServer.js.map +1 -1
- package/dist/esm/file-logger.d.ts +19 -0
- package/dist/esm/file-logger.d.ts.map +1 -0
- package/dist/esm/file-logger.js +156 -0
- package/dist/esm/file-logger.js.map +6 -0
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/package.json +9 -9
- package/src/MatterServer.ts +17 -25
- package/src/file-logger.ts +231 -0
- package/src/version.ts +2 -2
package/dist/esm/MatterServer.js
CHANGED
|
@@ -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
|
|
65
|
+
const fileLogger = await createFileLogger(cliOptions.logFile);
|
|
66
|
+
fileLoggerClose = fileLogger.close;
|
|
81
67
|
Logger.destinations.file = LogDestination({
|
|
82
|
-
write:
|
|
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": "AAAA;AAAA;AAAA;AAAA;AAAA;AAMA;AAAA,EACI;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACG;AACP,SAAS,
|
|
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
|
+
}
|
package/dist/esm/version.js
CHANGED
|
@@ -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");
|
package/dist/esm/version.js.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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-
|
|
27
|
-
"@matter-server/ws-controller": "0.5.
|
|
28
|
-
"@matter-server/dashboard": "0.5.
|
|
29
|
-
"@matter-server/custom-clusters": "0.5.
|
|
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.
|
|
36
|
-
"@matter/nodejs": "0.17.0-alpha.0-
|
|
37
|
-
"@matter/testing": "0.17.0-alpha.0-
|
|
38
|
-
"@matter-server/ws-client": "0.5.
|
|
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/**/*",
|
package/src/MatterServer.ts
CHANGED
|
@@ -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
|
|
86
|
+
const fileLogger = await createFileLogger(cliOptions.logFile);
|
|
87
|
+
fileLoggerClose = fileLogger.close;
|
|
107
88
|
Logger.destinations.file = LogDestination({
|
|
108
|
-
write:
|
|
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");
|