mobile-automator 0.26.0 → 0.28.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.
@@ -74,6 +74,26 @@ function lockPath(projectRoot) {
74
74
  return path.join(sessionDir(projectRoot), LOCK_NAME);
75
75
  }
76
76
 
77
+ // The workspace a daemon serves, as it appears on its command line (#156).
78
+ // The daemon itself reads its root from env (MAUTO_SESSION_PROJECT_ROOT), which
79
+ // `ps` cannot see, so every daemon's command line used to be identical. This
80
+ // marker is what lets `session end` tell THIS workspace's daemon from another
81
+ // workspace's before it signals a process group: a copied workspace (`cp -R`
82
+ // carries daemon.pid) or a recycled pid can name a different live daemon.
83
+ // The root is the LAST argument so a match can be anchored at the end.
84
+ const DAEMON_ROOT_FLAG = '--project-root';
85
+
86
+ function daemonArgv(projectRoot) {
87
+ return [DAEMON_ROOT_FLAG, path.resolve(projectRoot)];
88
+ }
89
+
90
+ // Does a `ps -o command=` line name exactly this workspace? Anchored at the end
91
+ // of the line so `/a/proj` never matches a daemon serving `/a/proj2`.
92
+ function commandNamesWorkspace(command, projectRoot) {
93
+ const line = String(command || '').replace(/\s+$/, '');
94
+ return line.endsWith(` ${DAEMON_ROOT_FLAG} ${path.resolve(projectRoot)}`);
95
+ }
96
+
77
97
  module.exports = {
78
98
  SESSION_DIRNAME,
79
99
  SOCKET_NAME,
@@ -89,4 +109,7 @@ module.exports = {
89
109
  pidFilePath,
90
110
  handlePath,
91
111
  lockPath,
112
+ DAEMON_ROOT_FLAG,
113
+ daemonArgv,
114
+ commandNamesWorkspace,
92
115
  };
@@ -12,6 +12,8 @@ const childProcess = require('child_process');
12
12
 
13
13
  const sessionClient = require('./session-client');
14
14
  const sessionLog = require('./session-log');
15
+ const paths = require('./session-paths');
16
+ const reaper = require('./process-reaper');
15
17
 
16
18
  const DAEMON_BIN = path.join(__dirname, '..', '..', 'bin', 'mauto-session-daemon.js');
17
19
  const DEFAULT_READY_TIMEOUT_MS = 15000;
@@ -31,6 +33,10 @@ async function spawnDaemon({
31
33
  pollMs = DEFAULT_POLL_MS,
32
34
  isAlive = sessionClient.isAlive,
33
35
  openLog = sessionLog.openDaemonLog,
36
+ // Kills a daemon that never became ready (#156). group:true reaches the
37
+ // daemon AND its mobile-mcp engine: `detached: true` below makes the daemon
38
+ // a process-group leader, and the engine is spawned into its group.
39
+ terminate = (pid) => reaper.terminate(pid, { group: true }),
34
40
  } = {}) {
35
41
  if (!projectRoot) throw new TypeError('spawnDaemon requires projectRoot');
36
42
 
@@ -46,7 +52,9 @@ async function spawnDaemon({
46
52
  const log = openLog(projectRoot);
47
53
 
48
54
  try {
49
- const child = spawn(process.execPath, [DAEMON_BIN], {
55
+ // The root in argv is an identity marker only (see paths.daemonArgv): env
56
+ // stays the daemon's source of truth, but `ps` can only see argv.
57
+ const child = spawn(process.execPath, [DAEMON_BIN, ...paths.daemonArgv(projectRoot)], {
50
58
  detached: true,
51
59
  stdio: log.stdio,
52
60
  env,
@@ -80,6 +88,18 @@ async function spawnDaemon({
80
88
  if (await isAlive(projectRoot)) return true;
81
89
  await sleep(pollMs);
82
90
  }
91
+
92
+ // Not ready in time. Without this the child lives on — typically still
93
+ // inside createCall, holding the O_EXCL lock — and every later verb burns
94
+ // its own readiness window against it (#156). Only OUR child, and only if
95
+ // it is still running: an ELOCKED spawn-race loser has already exited and
96
+ // must never signal anything. Node tracked this exact child, so its state
97
+ // needs no pidIdentity check and has no pid-reuse window.
98
+ if (child && child.pid && child.exitCode === null && child.signalCode === null) {
99
+ const outcome = await terminate(child.pid);
100
+ // The parent is the only witness to this kill — write it down (#163).
101
+ log.write(`mauto: daemon not ready after ${readyTimeoutMs}ms; terminated pid=${child.pid} (${outcome})\n`);
102
+ }
83
103
  return false;
84
104
  } finally {
85
105
  // The child holds its own dup, so the parent releases its descriptor rather
@@ -89,4 +109,4 @@ async function spawnDaemon({
89
109
  }
90
110
  }
91
111
 
92
- module.exports = { spawnDaemon, DAEMON_BIN };
112
+ module.exports = { spawnDaemon, DAEMON_BIN, DEFAULT_READY_TIMEOUT_MS };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ // Geometry for a direction-only swipe: start at centre + 30% of the axis
4
+ // dimension and travel 60% of it, i.e. centre ± 30% (#199).
5
+ //
6
+ // Why mauto computes this instead of letting the engine choose: mobile-mcp
7
+ // 0.0.55's adb and WDA robots used exactly this proportional travel, but 1.0.5
8
+ // routes every device through mobilecli, whose direction-only swipe is a fixed
9
+ // 400px centred stroke — about a third of the old travel on a phone, so
10
+ // scroll-to-element needs ~3x the swipes. Every robot in both versions reads an
11
+ // explicit {x, y, distance} the same way (start at x,y; move `distance` px in
12
+ // `direction`), so sending it pins identical behaviour on either pin.
13
+ //
14
+ // Kept pure and in its own module (not inside DeviceBridge) so the arithmetic
15
+ // is testable without an engine fake and the bridge stays a thin wrapper.
16
+ // There is deliberately no platform branch: proportional geometry is what makes
17
+ // it platform-agnostic.
18
+ //
19
+ // The integer formulas mirror 0.0.55's AndroidRobot (`Math.floor(n * 0.8)`,
20
+ // `Math.floor(n * 0.2)`, centre `floor(n / 2)`) so the endpoints match its old
21
+ // ones pixel-for-pixel, floating-point rounding included.
22
+ function centredSwipe({ width, height }, direction) {
23
+ const hi = (n) => Math.floor(n * 0.8);
24
+ const lo = (n) => Math.floor(n * 0.2);
25
+ const cx = Math.floor(width / 2);
26
+ const cy = Math.floor(height / 2);
27
+ switch (direction) {
28
+ case 'up': return { x: cx, y: hi(height), distance: hi(height) - lo(height) };
29
+ case 'down': return { x: cx, y: lo(height), distance: hi(height) - lo(height) };
30
+ case 'left': return { x: hi(width), y: cy, distance: hi(width) - lo(width) };
31
+ case 'right': return { x: lo(width), y: cy, distance: hi(width) - lo(width) };
32
+ default:
33
+ // The CLI validates directions first; this guards other callers from a
34
+ // swipe with undefined geometry.
35
+ throw new Error(`Unknown swipe direction "${direction}".`);
36
+ }
37
+ }
38
+
39
+ module.exports = { centredSwipe };
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
- // mobile-mcp 0.0.55 requires `device` on every tool EXCEPT the discovery verb.
3
+ // mobile-mcp 1.0.5 requires `device` on every tool EXCEPT the discovery verb;
4
+ // tests/integration/mobile-mcp-contract.test.js enforces this against the
5
+ // pinned engine's own tool schemas.
4
6
  // Centralizing the injection (and its one exception) here keeps every call
5
7
  // site correct. Pure + non-mutating so it is trivially testable.
6
8
  const NO_DEVICE_TOOLS = new Set(['mobile_list_available_devices']);
@@ -120,10 +120,10 @@ const EVENT_FIELDS = {
120
120
  // src/device/mobile-mcp-tools.js. A daemon-minted counter needs no redaction
121
121
  // argument at all, on exactly the grounds dur_ms is cleared.
122
122
  call_id: sendable(integer({ min: 1 }), 'daemon-minted counter scoped to one session; never the client-supplied frame id'),
123
- stop_reason: sendable(oneOf(STOP_REASONS), 'enumerated: idle|signal|shutdown|crash|explicit'),
123
+ stop_reason: sendable(oneOf(STOP_REASONS), 'enumerated: idle|signal|shutdown|crash|explicit|engine_exit'),
124
124
  // Errno NAMES only: the numeric codes an MCP server reports, and Node's
125
125
  // ERR_* codes, fail the check and are dropped rather than sent.
126
- error_code: sendable(oneOf(ERROR_CODES), 'Node/libuv errno string (EADDRINUSE|EACCES|…) plus our own ELOCKED'),
126
+ error_code: sendable(oneOf(ERROR_CODES), 'Node/libuv errno string (EADDRINUSE|EACCES|…) plus our own ELOCKED|ENGINE_CONNECT_TIMEOUT|ENGINE_EXITED'),
127
127
 
128
128
  // --- crash (slice 4) ---------------------------------------------------
129
129
  // The ONLY new field slice 4 mints. As with `verb`, `tool` and `session_id`,
@@ -43,6 +43,7 @@ const EVENT_NAMES = Object.freeze([
43
43
  'daemon.start_failure',
44
44
  'daemon.listen_failure',
45
45
  'daemon.connect_failure',
46
+ 'daemon.engine_exit',
46
47
  'daemon.lock_conflict',
47
48
  'daemon.undeliverable',
48
49
  // Emitted by the flush path, which lands in a later slice-5 task; declared
@@ -86,11 +87,20 @@ const VERB_NAMES = Object.freeze([
86
87
  ]);
87
88
 
88
89
  // Why the daemon stopped. 'crash' is documented but not yet produced.
89
- const STOP_REASONS = Object.freeze(['idle', 'signal', 'shutdown', 'crash', 'explicit']);
90
+ // 'engine_exit': the mobile-mcp engine died under a serving daemon (#156).
91
+ const STOP_REASONS = Object.freeze(['idle', 'signal', 'shutdown', 'crash', 'explicit', 'engine_exit']);
90
92
 
91
- // Node/libuv errno names for this platform, plus our own ELOCKED (a lock
92
- // conflict, recorded by src/device/session-daemon.js). Names only: the numeric codes an MCP
93
- // server reports are not in this set and so never pass.
94
- const ERROR_CODES = Object.freeze([...Object.keys(os.constants.errno), 'ELOCKED']);
93
+ // Node/libuv errno names for this platform, plus our own codes, all recorded by
94
+ // src/device/: ELOCKED (a lock conflict, session-daemon.js),
95
+ // ENGINE_CONNECT_TIMEOUT (initialize exceeded its bound, mobile-mcp-client.js)
96
+ // and ENGINE_EXITED (the engine died before the daemon was serving,
97
+ // session-daemon.js). Names only: the numeric codes an MCP server reports are
98
+ // not in this set and so never pass.
99
+ const ERROR_CODES = Object.freeze([
100
+ ...Object.keys(os.constants.errno),
101
+ 'ELOCKED',
102
+ 'ENGINE_CONNECT_TIMEOUT',
103
+ 'ENGINE_EXITED',
104
+ ]);
95
105
 
96
106
  module.exports = { OS_NAMES, EVENT_NAMES, VERB_NAMES, STOP_REASONS, ERROR_CODES };