mobile-automator 0.23.9 → 0.24.0
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/bin/mauto-session-daemon.js +9 -0
- package/package.json +1 -1
- package/src/cli.js +6 -1
- package/src/device/connection.js +20 -4
- package/src/device/resolve-connection.js +23 -3
- package/src/device/session-daemon.js +5 -1
- package/src/device/session-log.js +95 -0
- package/src/device/session-paths.js +12 -0
- package/src/device/session-spawn.js +51 -26
|
@@ -46,6 +46,15 @@ function cleanupWorkspaceIfOwned(projectRoot) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
async function main() {
|
|
49
|
+
// First write of the process, so every later line in the shared append-only
|
|
50
|
+
// log is attributable to a spawn. Written HERE rather than by the spawning
|
|
51
|
+
// parent because only this process knows its own pid, and because a parent
|
|
52
|
+
// banner races the child's own first output (#163). Under the degraded
|
|
53
|
+
// `stdio: 'ignore'` handle this lands in /dev/null, which is correct.
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`\n=== mauto daemon ${new Date().toISOString()} pid=${process.pid} ===\n`
|
|
56
|
+
);
|
|
57
|
+
|
|
49
58
|
const projectRoot = process.env.MAUTO_SESSION_PROJECT_ROOT;
|
|
50
59
|
if (!projectRoot) {
|
|
51
60
|
process.stderr.write('mauto-session-daemon: MAUTO_SESSION_PROJECT_ROOT is required\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-automator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Host-agnostic mauto CLI for AI-driven mobile QA automation — analyzes a mobile project and drives devices through mobile-mcp.",
|
|
5
5
|
"homepage": "https://github.com/sh3lan93/mobile-automator#readme",
|
|
6
6
|
"repository": {
|
package/src/cli.js
CHANGED
|
@@ -995,8 +995,13 @@ async function handleSessionStart(
|
|
|
995
995
|
|
|
996
996
|
const started = await connection.startSession({ projectRoot, device, idleMs, spawn });
|
|
997
997
|
if (!started) {
|
|
998
|
+
// The daemon's stdout/stderr are captured to the workspace log, so the
|
|
999
|
+
// "why" of a startup crash exists on disk — but only this hint tells the
|
|
1000
|
+
// user (or agent) that it does. Same sentence the transparent-autostart
|
|
1001
|
+
// fallback uses, so the log is named identically wherever it surfaces.
|
|
1002
|
+
const logHint = connection.daemonLogHint(projectRoot);
|
|
998
1003
|
return {
|
|
999
|
-
envelope: fail('device', 'failed to start the device session daemon',
|
|
1004
|
+
envelope: fail('device', 'failed to start the device session daemon', `Ensure a device or simulator is connected, or run verbs directly (they fall back to one-shot). ${logHint}`),
|
|
1000
1005
|
exitKind: 'device',
|
|
1001
1006
|
};
|
|
1002
1007
|
}
|
package/src/device/connection.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// handlers.
|
|
15
15
|
|
|
16
16
|
const { resolveDeviceConnection } = require('./resolve-connection');
|
|
17
|
+
const { daemonLogHint } = require('./session-log');
|
|
18
|
+
const paths = require('./session-paths');
|
|
17
19
|
const sessionClient = require('./session-client');
|
|
18
20
|
const sessionSpawn = require('./session-spawn');
|
|
19
21
|
|
|
@@ -40,10 +42,20 @@ function isSessionAlive(projectRoot, { client = sessionClient } = {}) {
|
|
|
40
42
|
return client.isAlive(projectRoot);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
// Rich status: running + in-flight call count + pinned device
|
|
44
|
-
// running. The in-flight
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Rich status: running + in-flight call count + pinned device (`null`s when not
|
|
46
|
+
// running), plus log_path, which is reported unconditionally. The in-flight
|
|
47
|
+
// count exposes the double-execution window.
|
|
48
|
+
//
|
|
49
|
+
// log_path is computed HERE from the project root rather than read out of the
|
|
50
|
+
// daemon's ping reply, and that is deliberate: getSessionStatus() collapses
|
|
51
|
+
// every failure branch — no socket, non-ok ping, or a throw — into the same
|
|
52
|
+
// not-running shape, so a daemon-supplied path would go missing in exactly the
|
|
53
|
+
// case where a user is reaching for `session status` to find out why the daemon
|
|
54
|
+
// is dead. The path is a pure function of the workspace, so we can always
|
|
55
|
+
// answer it. Do not "simplify" this into the ping payload.
|
|
56
|
+
async function sessionStatus(projectRoot, { client = sessionClient } = {}) {
|
|
57
|
+
const status = await client.getSessionStatus(projectRoot);
|
|
58
|
+
return { ...status, log_path: paths.logFilePath(projectRoot) };
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
// Spawn a daemon and wait until it answers. Resolves true on success.
|
|
@@ -58,6 +70,10 @@ function endSession(projectRoot, { client = sessionClient } = {}) {
|
|
|
58
70
|
|
|
59
71
|
module.exports = {
|
|
60
72
|
acquireConnection,
|
|
73
|
+
// Re-exported from session-log so cli.js can name the daemon log through the
|
|
74
|
+
// one device facade it already imports, instead of reaching into a
|
|
75
|
+
// connection-strategy module for a string.
|
|
76
|
+
daemonLogHint,
|
|
61
77
|
isSessionAlive,
|
|
62
78
|
sessionStatus,
|
|
63
79
|
startSession,
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// (c) autostart && no reachable daemon
|
|
15
15
|
// -> spawn a daemon, then connect daemon-backed (no-op close).
|
|
16
16
|
// (d) spawn failed, or autostart:false
|
|
17
|
-
// -> one-shot fallback via the real createCall (real close).
|
|
17
|
+
// -> one-shot fallback via the real createCall (real close). When the
|
|
18
|
+
// spawn is what failed, a thrown one-shot error is decorated with a
|
|
19
|
+
// hint naming the daemon log so the crash stays findable (#163).
|
|
18
20
|
//
|
|
19
21
|
// Everything device-touching is injected so the whole matrix is unit-testable
|
|
20
22
|
// with a fake daemon + fake createCall + fake spawn.
|
|
@@ -24,6 +26,7 @@ const fs = require('fs');
|
|
|
24
26
|
const { DeviceBridge } = require('./bridge');
|
|
25
27
|
const paths = require('./session-paths');
|
|
26
28
|
const sessionClient = require('./session-client');
|
|
29
|
+
const { daemonLogHint } = require('./session-log');
|
|
27
30
|
const sessionSpawn = require('./session-spawn');
|
|
28
31
|
|
|
29
32
|
function defaultCreateCall(opts) {
|
|
@@ -135,17 +138,34 @@ async function resolveDeviceConnection({
|
|
|
135
138
|
requestedDevice: device,
|
|
136
139
|
autostart,
|
|
137
140
|
});
|
|
141
|
+
let spawnFailed = false;
|
|
138
142
|
if (strategy === 'spawn-then-daemon') {
|
|
139
143
|
// (c) spawn a daemon, then connect daemon-backed.
|
|
140
144
|
const started = await spawn.spawnDaemon({ projectRoot, device, idleMs });
|
|
141
145
|
if (started) {
|
|
142
146
|
const conn = await daemonBacked();
|
|
143
147
|
if (conn) return conn;
|
|
148
|
+
// Spawn worked; the daemon just wasn't reachable. Its log has nothing to
|
|
149
|
+
// say about a subsequent one-shot failure, so no hint — fall through.
|
|
150
|
+
} else {
|
|
151
|
+
spawnFailed = true;
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
// (d) explicit one-shot, or spawn failed / unreachable.
|
|
148
|
-
|
|
155
|
+
// (d) explicit one-shot, or spawn failed / unreachable. When the SPAWN is
|
|
156
|
+
// what failed, the daemon's log is the only record of why, and transparent
|
|
157
|
+
// autostart is the common path — most users never type `mauto session start`,
|
|
158
|
+
// so without this the log stays invisible to almost everyone. The signal
|
|
159
|
+
// rides err.hint because deviceFail() in cli.js already surfaces it on every
|
|
160
|
+
// device verb, so acquireConnection's deliberately narrow contract is
|
|
161
|
+
// untouched (#163).
|
|
162
|
+
try {
|
|
163
|
+
return await oneShot();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// Never clobber a more specific hint the transport already attached.
|
|
166
|
+
if (spawnFailed && !err.hint) err.hint = daemonLogHint(projectRoot);
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
module.exports = {
|
|
@@ -225,7 +225,11 @@ async function startDaemon({
|
|
|
225
225
|
`mauto-session-daemon: undeliverable reply id=${info.id}: ${info.reason}\n`
|
|
226
226
|
);
|
|
227
227
|
} catch (_) {
|
|
228
|
-
|
|
228
|
+
// fd 2 is unwritable (full disk, or the workspace log could not be
|
|
229
|
+
// opened and this is /dev/null's successor). Since #163 it normally
|
|
230
|
+
// reaches mobile-automator/.session/daemon.log; either way an
|
|
231
|
+
// undeliverable reply is already recorded in `undeliverable`, so
|
|
232
|
+
// losing the warning must not take the daemon down.
|
|
229
233
|
}
|
|
230
234
|
};
|
|
231
235
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Lifecycle of the daemon's log file, so session-spawn.js can stay
|
|
4
|
+
// filesystem-free and just hand the stdio triple to child_process.spawn.
|
|
5
|
+
//
|
|
6
|
+
// Failure policy is PER-STEP, not whole-module. Rotation is a disk guard;
|
|
7
|
+
// the diagnostics are the point. A rotation that cannot run — a race with a
|
|
8
|
+
// concurrent spawn, a permissions problem — must not cost us the log. Only an
|
|
9
|
+
// unopenable log degrades, and it degrades to today's `stdio: 'ignore'`:
|
|
10
|
+
// a read-only or full workspace loses diagnostics, never the CLI.
|
|
11
|
+
|
|
12
|
+
const realFs = require('fs');
|
|
13
|
+
|
|
14
|
+
const { sessionDir, logFilePath } = require('./session-paths');
|
|
15
|
+
|
|
16
|
+
const MAX_LOG_BYTES = 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
// The degraded handle, expressed as a VALUE so callers never branch on null.
|
|
19
|
+
// `stdio: 'ignore'` is the behaviour that predates #163.
|
|
20
|
+
const IGNORED = Object.freeze({ stdio: 'ignore', write() {}, close() {} });
|
|
21
|
+
|
|
22
|
+
// The one sentence pointing a user at the daemon's captured output (#163).
|
|
23
|
+
// It lives here because it is a fact about the log, and it is re-exported
|
|
24
|
+
// through connection.js so cli.js reaches it via the device facade it already
|
|
25
|
+
// imports rather than reaching into a connection-strategy module.
|
|
26
|
+
function daemonLogHint(projectRoot) {
|
|
27
|
+
return `The daemon's output (including stack traces) was captured to ${logFilePath(projectRoot)}.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Best-effort rotation to a single previous generation; clobbering an existing
|
|
31
|
+
// .1 is the intended bound. Every failure here is survivable — no log yet
|
|
32
|
+
// (ENOENT on stat), a concurrent spawn that already rotated (ENOENT on rename),
|
|
33
|
+
// an unreadable directory (EACCES) — so we swallow and carry on to the open.
|
|
34
|
+
// An oversized log beats no log.
|
|
35
|
+
function rotateIfLarge(logPath, maxBytes, fs) {
|
|
36
|
+
try {
|
|
37
|
+
if (fs.statSync(logPath).size >= maxBytes) fs.renameSync(logPath, `${logPath}.1`);
|
|
38
|
+
} catch (_) {
|
|
39
|
+
/* nothing to rotate, or someone beat us to it */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Returns { stdio, write, close } — NEVER null.
|
|
44
|
+
//
|
|
45
|
+
// stdio the triple for child_process.spawn. stdout AND stderr point at the
|
|
46
|
+
// same fd on purpose: the child gets dups sharing one O_APPEND file
|
|
47
|
+
// description, so the two streams interleave without clobbering. That
|
|
48
|
+
// one descriptor also captures mobile-mcp, whose MCP stdio transport
|
|
49
|
+
// defaults its stderr to 'inherit' and therefore writes to whatever
|
|
50
|
+
// fd 2 the daemon has. Point fd 2 at /dev/null and its crashes vanish.
|
|
51
|
+
// write for diagnostics only the PARENT can see. A spawn that fails to exec
|
|
52
|
+
// leaves the child with nothing to say, so this is the sole record of
|
|
53
|
+
// EMFILE/EACCES/ENOENT-on-the-bin (#163).
|
|
54
|
+
// close releases the parent's dup; the child holds its own.
|
|
55
|
+
function openDaemonLog(projectRoot, { maxBytes = MAX_LOG_BYTES, fs = realFs } = {}) {
|
|
56
|
+
let fd;
|
|
57
|
+
try {
|
|
58
|
+
// The parent must create .session/: only startDaemon creates it today, and
|
|
59
|
+
// that runs in the child — too late for an fd the parent has to open first.
|
|
60
|
+
fs.mkdirSync(sessionDir(projectRoot), { recursive: true });
|
|
61
|
+
|
|
62
|
+
const logPath = logFilePath(projectRoot);
|
|
63
|
+
rotateIfLarge(logPath, maxBytes, fs);
|
|
64
|
+
|
|
65
|
+
// Append, never 'w'. Lock-race losers exit ELOCKED through
|
|
66
|
+
// bin/mauto-session-daemon.js and write to this same file; truncating on
|
|
67
|
+
// open would let a loser erase the winner's just-written crash trace.
|
|
68
|
+
fd = fs.openSync(logPath, 'a');
|
|
69
|
+
} catch (_) {
|
|
70
|
+
return IGNORED;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const write = (text) => {
|
|
74
|
+
try {
|
|
75
|
+
fs.writeSync(fd, text);
|
|
76
|
+
} catch (_) {
|
|
77
|
+
// Full disk, closed fd — the diagnostics are optional, the spawn is not.
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let closed = false;
|
|
82
|
+
const close = () => {
|
|
83
|
+
if (closed) return;
|
|
84
|
+
closed = true;
|
|
85
|
+
try {
|
|
86
|
+
fs.closeSync(fd);
|
|
87
|
+
} catch (_) {
|
|
88
|
+
// Already reaped by the runtime; nothing left to do.
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return { stdio: ['ignore', fd, fd], write, close };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { MAX_LOG_BYTES, IGNORED, daemonLogHint, openDaemonLog };
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// mauto.sock — Unix domain socket the daemon listens on
|
|
7
7
|
// daemon.pid — pidfile written once the daemon is listening
|
|
8
8
|
// session.json — handle describing the live session (device pin, socket, pid)
|
|
9
|
+
// daemon.log — bounded stdout/stderr capture from the detached daemon
|
|
9
10
|
//
|
|
10
11
|
// These are intentionally side-effect-free so they can be unit-tested without
|
|
11
12
|
// touching the filesystem.
|
|
@@ -19,6 +20,7 @@ const SOCKET_NAME = 'mauto.sock';
|
|
|
19
20
|
const PID_NAME = 'daemon.pid';
|
|
20
21
|
const HANDLE_NAME = 'session.json';
|
|
21
22
|
const LOCK_NAME = 'daemon.lock';
|
|
23
|
+
const LOG_NAME = 'daemon.log';
|
|
22
24
|
|
|
23
25
|
// Unix domain socket paths have a hard length limit (~104 bytes on macOS,
|
|
24
26
|
// ~108 on Linux). Deep project roots blow past it, so when the in-workspace
|
|
@@ -50,6 +52,14 @@ function socketPath(projectRoot) {
|
|
|
50
52
|
return path.join(os.tmpdir(), `mauto-${hash}.sock`);
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// Deliberately has no os.tmpdir() fallback, unlike socketPath(). That fallback
|
|
56
|
+
// exists solely because Unix domain socket paths hit a ~104-byte sockaddr
|
|
57
|
+
// limit; a regular file has no such limit, so the log stays in the workspace
|
|
58
|
+
// unconditionally and is always findable next to the session it describes.
|
|
59
|
+
function logFilePath(projectRoot) {
|
|
60
|
+
return path.join(sessionDir(projectRoot), LOG_NAME);
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
function pidFilePath(projectRoot) {
|
|
54
64
|
return path.join(sessionDir(projectRoot), PID_NAME);
|
|
55
65
|
}
|
|
@@ -70,10 +80,12 @@ module.exports = {
|
|
|
70
80
|
PID_NAME,
|
|
71
81
|
HANDLE_NAME,
|
|
72
82
|
LOCK_NAME,
|
|
83
|
+
LOG_NAME,
|
|
73
84
|
SOCKET_PATH_LIMIT,
|
|
74
85
|
sessionDir,
|
|
75
86
|
workspaceSocketPath,
|
|
76
87
|
socketPath,
|
|
88
|
+
logFilePath,
|
|
77
89
|
pidFilePath,
|
|
78
90
|
handlePath,
|
|
79
91
|
lockPath,
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// Spawns the device session daemon as a DETACHED background process and waits
|
|
4
|
-
// until it is reachable. The
|
|
5
|
-
//
|
|
4
|
+
// until it is reachable. The daemon's output is captured to the workspace log
|
|
5
|
+
// (mobile-automator/.session/daemon.log) instead of /dev/null, so a crashing
|
|
6
|
+
// engine leaves a readable trace behind. The spawn and openLog fns are
|
|
7
|
+
// injectable so the spawn args, the log wiring and the readiness-poll contract
|
|
8
|
+
// are all unit-testable without launching a real daemon or touching disk.
|
|
6
9
|
|
|
7
10
|
const path = require('path');
|
|
8
11
|
const childProcess = require('child_process');
|
|
9
12
|
|
|
10
13
|
const sessionClient = require('./session-client');
|
|
14
|
+
const sessionLog = require('./session-log');
|
|
11
15
|
|
|
12
16
|
const DAEMON_BIN = path.join(__dirname, '..', '..', 'bin', 'mauto-session-daemon.js');
|
|
13
17
|
const DEFAULT_READY_TIMEOUT_MS = 15000;
|
|
@@ -26,6 +30,7 @@ async function spawnDaemon({
|
|
|
26
30
|
readyTimeoutMs = DEFAULT_READY_TIMEOUT_MS,
|
|
27
31
|
pollMs = DEFAULT_POLL_MS,
|
|
28
32
|
isAlive = sessionClient.isAlive,
|
|
33
|
+
openLog = sessionLog.openDaemonLog,
|
|
29
34
|
} = {}) {
|
|
30
35
|
if (!projectRoot) throw new TypeError('spawnDaemon requires projectRoot');
|
|
31
36
|
|
|
@@ -33,35 +38,55 @@ async function spawnDaemon({
|
|
|
33
38
|
if (device) env.MAUTO_SESSION_DEVICE = device;
|
|
34
39
|
if (idleMs !== undefined && idleMs !== null) env.MAUTO_SESSION_IDLE_MS = String(idleMs);
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// Must precede the spawn: the fd has to exist before the child can inherit it.
|
|
42
|
+
// Never null — a workspace we cannot write to yields the `stdio: 'ignore'`
|
|
43
|
+
// handle, so there is nothing to branch on here. The daemon writes its own
|
|
44
|
+
// spawn banner (bin/mauto-session-daemon.js), which is why this file no
|
|
45
|
+
// longer needs the child's pid.
|
|
46
|
+
const log = openLog(projectRoot);
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// expected, and the winner may still come up within the poll window.
|
|
48
|
-
let spawnError = null;
|
|
49
|
-
if (child && typeof child.on === 'function') {
|
|
50
|
-
child.on('error', (err) => {
|
|
51
|
-
spawnError = err;
|
|
48
|
+
try {
|
|
49
|
+
const child = spawn(process.execPath, [DAEMON_BIN], {
|
|
50
|
+
detached: true,
|
|
51
|
+
stdio: log.stdio,
|
|
52
|
+
env,
|
|
52
53
|
});
|
|
53
|
-
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// A spawn that fails to exec (EMFILE, EACCES, ENOENT on the bin) emits an
|
|
56
|
+
// 'error' event instead of throwing. Without a listener that error would
|
|
57
|
+
// crash the one-shot verb process; with it we bail out of the readiness
|
|
58
|
+
// poll immediately instead of waiting out the full 15s window for a child
|
|
59
|
+
// that can never come up. No 'exit' short-circuit here: a loser exiting
|
|
60
|
+
// ELOCKED is expected, and the winner may still come up within the window.
|
|
61
|
+
//
|
|
62
|
+
// The error is also WRITTEN to the log, not just used as a flag: a child
|
|
63
|
+
// that never execs has no stdout/stderr of its own, so the parent is the
|
|
64
|
+
// only witness. Dropping it here would rebuild the exact hole #163 fixes,
|
|
65
|
+
// one layer up.
|
|
66
|
+
let spawnError = null;
|
|
67
|
+
if (child && typeof child.on === 'function') {
|
|
68
|
+
child.on('error', (err) => {
|
|
69
|
+
spawnError = err;
|
|
70
|
+
log.write(`mauto: daemon spawn failed: ${err && err.stack ? err.stack : err}\n`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Let the daemon outlive this one-shot verb process.
|
|
75
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
if (spawnError) return false;
|
|
80
|
+
if (await isAlive(projectRoot)) return true;
|
|
81
|
+
await sleep(pollMs);
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
} finally {
|
|
85
|
+
// The child holds its own dup, so the parent releases its descriptor rather
|
|
86
|
+
// than leaking one per verb invocation. In `finally` because the 'error'
|
|
87
|
+
// listener above fires asynchronously — the handle has to outlive the poll.
|
|
88
|
+
log.close();
|
|
63
89
|
}
|
|
64
|
-
return false;
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
module.exports = { spawnDaemon, DAEMON_BIN };
|