@remcp/runtime 0.2.152 → 0.2.154
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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/extended/diagnostics-audio.mjs +237 -0
- package/src/extended/diagnostics-platform.mjs +308 -0
- package/src/extended/diagnostics.mjs +4 -489
- package/src/extended/documents-filesystem.mjs +270 -0
- package/src/extended/documents-reading.mjs +163 -0
- package/src/extended/documents.mjs +3 -386
- package/src/helpers/wayland-eis-helper.py +2 -339
- package/src/helpers/wayland_eis_sender.py +344 -0
- package/src/wayland-remote-desktop-input.mjs +168 -0
- package/src/wayland-remote-desktop-utils.mjs +27 -0
- package/src/wayland-remote-desktop.mjs +11 -180
|
@@ -5,13 +5,14 @@ Owns the portal session and EIS file descriptor in one process so SCM_RIGHTS nev
|
|
|
5
5
|
has to cross Node's net.Socket implementation. JSON-lines are used only between
|
|
6
6
|
this helper and the local ReMCP runtime.
|
|
7
7
|
"""
|
|
8
|
-
import ctypes
|
|
9
8
|
import json
|
|
10
9
|
import os
|
|
11
10
|
import select
|
|
12
11
|
import sys
|
|
13
12
|
import time
|
|
14
13
|
|
|
14
|
+
from wayland_eis_sender import EiSender
|
|
15
|
+
|
|
15
16
|
try:
|
|
16
17
|
import gi
|
|
17
18
|
gi.require_version("Gio", "2.0")
|
|
@@ -27,21 +28,6 @@ REQUEST_IFACE = "org.freedesktop.portal.Request"
|
|
|
27
28
|
SESSION_IFACE = "org.freedesktop.portal.Session"
|
|
28
29
|
DEVICE_TYPES = 1 | 2 # keyboard | pointer in the portal API
|
|
29
30
|
|
|
30
|
-
EI_CAP_POINTER = 1 << 0
|
|
31
|
-
EI_CAP_POINTER_ABSOLUTE = 1 << 1
|
|
32
|
-
EI_CAP_KEYBOARD = 1 << 2
|
|
33
|
-
EI_CAP_SCROLL = 1 << 4
|
|
34
|
-
EI_CAP_BUTTON = 1 << 5
|
|
35
|
-
EI_EVENT_CONNECT = 1
|
|
36
|
-
EI_EVENT_DISCONNECT = 2
|
|
37
|
-
EI_EVENT_SEAT_ADDED = 3
|
|
38
|
-
EI_EVENT_SEAT_REMOVED = 4
|
|
39
|
-
EI_EVENT_DEVICE_ADDED = 5
|
|
40
|
-
EI_EVENT_DEVICE_REMOVED = 6
|
|
41
|
-
EI_EVENT_DEVICE_PAUSED = 7
|
|
42
|
-
EI_EVENT_DEVICE_RESUMED = 8
|
|
43
|
-
|
|
44
|
-
|
|
45
31
|
def emit(payload):
|
|
46
32
|
sys.stdout.write(json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + "\n")
|
|
47
33
|
sys.stdout.flush()
|
|
@@ -186,329 +172,6 @@ class Portal:
|
|
|
186
172
|
pass
|
|
187
173
|
|
|
188
174
|
|
|
189
|
-
class EiSender:
|
|
190
|
-
def __init__(self, fd):
|
|
191
|
-
self.lib = ctypes.CDLL("libei.so.1")
|
|
192
|
-
c_void_p = ctypes.c_void_p
|
|
193
|
-
self.lib.ei_new_sender.argtypes = [c_void_p]
|
|
194
|
-
self.lib.ei_new_sender.restype = c_void_p
|
|
195
|
-
self.lib.ei_configure_name.argtypes = [c_void_p, ctypes.c_char_p]
|
|
196
|
-
self.lib.ei_setup_backend_fd.argtypes = [c_void_p, ctypes.c_int]
|
|
197
|
-
self.lib.ei_setup_backend_fd.restype = ctypes.c_int
|
|
198
|
-
self.lib.ei_get_fd.argtypes = [c_void_p]
|
|
199
|
-
self.lib.ei_get_fd.restype = ctypes.c_int
|
|
200
|
-
self.lib.ei_dispatch.argtypes = [c_void_p]
|
|
201
|
-
self.lib.ei_get_event.argtypes = [c_void_p]
|
|
202
|
-
self.lib.ei_get_event.restype = c_void_p
|
|
203
|
-
self.lib.ei_event_get_type.argtypes = [c_void_p]
|
|
204
|
-
self.lib.ei_event_get_type.restype = ctypes.c_int
|
|
205
|
-
self.lib.ei_event_get_seat.argtypes = [c_void_p]
|
|
206
|
-
self.lib.ei_event_get_seat.restype = c_void_p
|
|
207
|
-
self.lib.ei_event_get_device.argtypes = [c_void_p]
|
|
208
|
-
self.lib.ei_event_get_device.restype = c_void_p
|
|
209
|
-
self.lib.ei_event_unref.argtypes = [c_void_p]
|
|
210
|
-
self.lib.ei_event_unref.restype = c_void_p
|
|
211
|
-
self.lib.ei_device_ref.argtypes = [c_void_p]
|
|
212
|
-
self.lib.ei_device_ref.restype = c_void_p
|
|
213
|
-
self.lib.ei_device_unref.argtypes = [c_void_p]
|
|
214
|
-
self.lib.ei_device_unref.restype = c_void_p
|
|
215
|
-
self.lib.ei_device_has_capability.argtypes = [c_void_p, ctypes.c_int]
|
|
216
|
-
self.lib.ei_device_has_capability.restype = ctypes.c_bool
|
|
217
|
-
self.lib.ei_device_get_region_at.argtypes = [c_void_p, ctypes.c_double, ctypes.c_double]
|
|
218
|
-
self.lib.ei_device_get_region_at.restype = c_void_p
|
|
219
|
-
self.lib.ei_region_convert_point.argtypes = [c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double)]
|
|
220
|
-
self.lib.ei_region_convert_point.restype = ctypes.c_bool
|
|
221
|
-
self.lib.ei_device_start_emulating.argtypes = [c_void_p, ctypes.c_uint32]
|
|
222
|
-
self.lib.ei_device_stop_emulating.argtypes = [c_void_p]
|
|
223
|
-
self.lib.ei_device_keyboard_key.argtypes = [c_void_p, ctypes.c_uint32, ctypes.c_bool]
|
|
224
|
-
self.lib.ei_device_pointer_motion.argtypes = [c_void_p, ctypes.c_double, ctypes.c_double]
|
|
225
|
-
self.lib.ei_device_pointer_motion_absolute.argtypes = [c_void_p, ctypes.c_double, ctypes.c_double]
|
|
226
|
-
self.lib.ei_device_button_button.argtypes = [c_void_p, ctypes.c_uint32, ctypes.c_bool]
|
|
227
|
-
self.lib.ei_device_scroll_discrete.argtypes = [c_void_p, ctypes.c_int32, ctypes.c_int32]
|
|
228
|
-
self.lib.ei_device_frame.argtypes = [c_void_p, ctypes.c_uint64]
|
|
229
|
-
self.lib.ei_now.argtypes = [c_void_p]
|
|
230
|
-
self.lib.ei_now.restype = ctypes.c_uint64
|
|
231
|
-
self.lib.ei_unref.argtypes = [c_void_p]
|
|
232
|
-
self.lib.ei_unref.restype = c_void_p
|
|
233
|
-
self.lib.ei_seat_has_capability.argtypes = [c_void_p, ctypes.c_int]
|
|
234
|
-
self.lib.ei_seat_has_capability.restype = ctypes.c_bool
|
|
235
|
-
# ei_seat_bind_capabilities is varargs: keep argtypes unset.
|
|
236
|
-
self.lib.ei_seat_bind_capabilities.restype = None
|
|
237
|
-
|
|
238
|
-
self.ei = self.lib.ei_new_sender(None)
|
|
239
|
-
if not self.ei:
|
|
240
|
-
raise RuntimeError("ei_new_sender failed")
|
|
241
|
-
self.lib.ei_configure_name(self.ei, b"ReMCP")
|
|
242
|
-
rc = int(self.lib.ei_setup_backend_fd(self.ei, fd))
|
|
243
|
-
if rc < 0:
|
|
244
|
-
raise RuntimeError(f"ei_setup_backend_fd failed ({rc})")
|
|
245
|
-
self.fd = int(self.lib.ei_get_fd(self.ei))
|
|
246
|
-
if self.fd < 0:
|
|
247
|
-
raise RuntimeError("ei_get_fd failed")
|
|
248
|
-
self.devices = {}
|
|
249
|
-
self.sequence = 1
|
|
250
|
-
self.connected = False
|
|
251
|
-
self.pointer_device_key = None
|
|
252
|
-
self.pointer_position = None
|
|
253
|
-
# A press/release pair must stay on the same emulated device even if
|
|
254
|
-
# absolute motion crosses an EIS region and changes pointer_device_key.
|
|
255
|
-
self.button_device_keys = {}
|
|
256
|
-
|
|
257
|
-
def _device_key(self, ptr):
|
|
258
|
-
return int(ptr or 0)
|
|
259
|
-
|
|
260
|
-
def _remember_device(self, ptr):
|
|
261
|
-
key = self._device_key(ptr)
|
|
262
|
-
if not key:
|
|
263
|
-
return None
|
|
264
|
-
if key not in self.devices:
|
|
265
|
-
retained = self.lib.ei_device_ref(ptr)
|
|
266
|
-
self.devices[key] = {"ptr": retained, "resumed": False, "emulating": False}
|
|
267
|
-
return self.devices[key]
|
|
268
|
-
|
|
269
|
-
def _drop_device(self, ptr):
|
|
270
|
-
key = self._device_key(ptr)
|
|
271
|
-
state = self.devices.pop(key, None)
|
|
272
|
-
if key == self.pointer_device_key:
|
|
273
|
-
self.pointer_device_key = None
|
|
274
|
-
self.pointer_position = None
|
|
275
|
-
for button, device_key in list(self.button_device_keys.items()):
|
|
276
|
-
if device_key == key:
|
|
277
|
-
self.button_device_keys.pop(button, None)
|
|
278
|
-
if state:
|
|
279
|
-
if state["emulating"]:
|
|
280
|
-
try:
|
|
281
|
-
self.lib.ei_device_stop_emulating(state["ptr"])
|
|
282
|
-
except Exception:
|
|
283
|
-
pass
|
|
284
|
-
self.lib.ei_device_unref(state["ptr"])
|
|
285
|
-
|
|
286
|
-
def _bind_seat(self, seat):
|
|
287
|
-
caps = [EI_CAP_POINTER, EI_CAP_POINTER_ABSOLUTE, EI_CAP_KEYBOARD, EI_CAP_SCROLL, EI_CAP_BUTTON]
|
|
288
|
-
args = [ctypes.c_void_p(seat)]
|
|
289
|
-
args.extend(ctypes.c_int(cap) for cap in caps if self.lib.ei_seat_has_capability(seat, cap))
|
|
290
|
-
args.append(ctypes.c_void_p())
|
|
291
|
-
self.lib.ei_seat_bind_capabilities(*args)
|
|
292
|
-
|
|
293
|
-
def process_events(self):
|
|
294
|
-
self.lib.ei_dispatch(self.ei)
|
|
295
|
-
while True:
|
|
296
|
-
event = self.lib.ei_get_event(self.ei)
|
|
297
|
-
if not event:
|
|
298
|
-
break
|
|
299
|
-
try:
|
|
300
|
-
event_type = int(self.lib.ei_event_get_type(event))
|
|
301
|
-
if event_type == EI_EVENT_CONNECT:
|
|
302
|
-
self.connected = True
|
|
303
|
-
elif event_type == EI_EVENT_DISCONNECT:
|
|
304
|
-
raise RuntimeError("EIS server disconnected")
|
|
305
|
-
elif event_type == EI_EVENT_SEAT_ADDED:
|
|
306
|
-
seat = self.lib.ei_event_get_seat(event)
|
|
307
|
-
if seat:
|
|
308
|
-
self._bind_seat(seat)
|
|
309
|
-
elif event_type == EI_EVENT_DEVICE_ADDED:
|
|
310
|
-
device = self.lib.ei_event_get_device(event)
|
|
311
|
-
if device:
|
|
312
|
-
self._remember_device(device)
|
|
313
|
-
elif event_type == EI_EVENT_DEVICE_RESUMED:
|
|
314
|
-
device = self.lib.ei_event_get_device(event)
|
|
315
|
-
state = self._remember_device(device)
|
|
316
|
-
if state:
|
|
317
|
-
state["resumed"] = True
|
|
318
|
-
if not state["emulating"]:
|
|
319
|
-
self.lib.ei_device_start_emulating(state["ptr"], self.sequence)
|
|
320
|
-
self.sequence = (self.sequence + 1) & 0xFFFFFFFF or 1
|
|
321
|
-
state["emulating"] = True
|
|
322
|
-
elif event_type == EI_EVENT_DEVICE_PAUSED:
|
|
323
|
-
device = self.lib.ei_event_get_device(event)
|
|
324
|
-
key = self._device_key(device)
|
|
325
|
-
state = self.devices.get(key)
|
|
326
|
-
if state:
|
|
327
|
-
state["resumed"] = False
|
|
328
|
-
state["emulating"] = False
|
|
329
|
-
for button, device_key in list(self.button_device_keys.items()):
|
|
330
|
-
if device_key == key:
|
|
331
|
-
self.button_device_keys.pop(button, None)
|
|
332
|
-
elif event_type == EI_EVENT_DEVICE_REMOVED:
|
|
333
|
-
self._drop_device(self.lib.ei_event_get_device(event))
|
|
334
|
-
finally:
|
|
335
|
-
self.lib.ei_event_unref(event)
|
|
336
|
-
|
|
337
|
-
def wait_ready(self, timeout_s=10.0):
|
|
338
|
-
deadline = time.monotonic() + timeout_s
|
|
339
|
-
while time.monotonic() < deadline:
|
|
340
|
-
if (
|
|
341
|
-
self.connected
|
|
342
|
-
and self.capability_ready(EI_CAP_KEYBOARD)
|
|
343
|
-
and self.capability_ready(EI_CAP_POINTER)
|
|
344
|
-
and self.capability_ready(EI_CAP_BUTTON)
|
|
345
|
-
and self.capability_ready(EI_CAP_SCROLL)
|
|
346
|
-
):
|
|
347
|
-
return
|
|
348
|
-
readable, _, _ = select.select([self.fd], [], [], min(0.1, max(0.0, deadline - time.monotonic())))
|
|
349
|
-
if readable:
|
|
350
|
-
self.process_events()
|
|
351
|
-
raise TimeoutError(f"EIS devices did not become ready; capabilities={self.capabilities()}")
|
|
352
|
-
|
|
353
|
-
def capability_ready(self, cap):
|
|
354
|
-
return any(state["resumed"] and self.lib.ei_device_has_capability(state["ptr"], cap) for state in self.devices.values())
|
|
355
|
-
|
|
356
|
-
def capabilities(self):
|
|
357
|
-
names = {
|
|
358
|
-
EI_CAP_POINTER: "pointer",
|
|
359
|
-
EI_CAP_POINTER_ABSOLUTE: "pointer_absolute",
|
|
360
|
-
EI_CAP_KEYBOARD: "keyboard",
|
|
361
|
-
EI_CAP_SCROLL: "scroll",
|
|
362
|
-
EI_CAP_BUTTON: "button",
|
|
363
|
-
}
|
|
364
|
-
return {name: self.capability_ready(cap) for cap, name in names.items()}
|
|
365
|
-
|
|
366
|
-
def device_for(self, cap):
|
|
367
|
-
for state in self.devices.values():
|
|
368
|
-
if state["resumed"] and state["emulating"] and self.lib.ei_device_has_capability(state["ptr"], cap):
|
|
369
|
-
return state["ptr"]
|
|
370
|
-
raise RuntimeError(f"no resumed EIS device with capability {cap}")
|
|
371
|
-
|
|
372
|
-
def _absolute_motion_for_device(self, device, x, y):
|
|
373
|
-
if not self.lib.ei_device_has_capability(device, EI_CAP_POINTER_ABSOLUTE):
|
|
374
|
-
return None
|
|
375
|
-
region = self.lib.ei_device_get_region_at(device, float(x), float(y))
|
|
376
|
-
if not region:
|
|
377
|
-
return None
|
|
378
|
-
motion_x = ctypes.c_double(float(x))
|
|
379
|
-
motion_y = ctypes.c_double(float(y))
|
|
380
|
-
if not self.lib.ei_region_convert_point(region, ctypes.byref(motion_x), ctypes.byref(motion_y)):
|
|
381
|
-
raise RuntimeError(f"failed to convert desktop coordinate ({x}, {y}) into the selected EIS region")
|
|
382
|
-
return motion_x.value, motion_y.value
|
|
383
|
-
|
|
384
|
-
def device_for_absolute(self, x, y):
|
|
385
|
-
candidates = []
|
|
386
|
-
|
|
387
|
-
# Preserve pointer identity across absolute motion whenever the current
|
|
388
|
-
# device still covers the destination. This is required for pointer
|
|
389
|
-
# capture during drags: switching EIS devices mid-gesture can drop the
|
|
390
|
-
# captured pointer before its button release is observed.
|
|
391
|
-
preferred = self.devices.get(self.pointer_device_key)
|
|
392
|
-
if preferred and preferred["resumed"] and preferred["emulating"]:
|
|
393
|
-
device = preferred["ptr"]
|
|
394
|
-
if self.lib.ei_device_has_capability(device, EI_CAP_POINTER_ABSOLUTE):
|
|
395
|
-
candidates.append(device)
|
|
396
|
-
converted = self._absolute_motion_for_device(device, x, y)
|
|
397
|
-
if converted is not None:
|
|
398
|
-
return device, converted[0], converted[1]
|
|
399
|
-
|
|
400
|
-
for state in self.devices.values():
|
|
401
|
-
if not state["resumed"] or not state["emulating"]:
|
|
402
|
-
continue
|
|
403
|
-
device = state["ptr"]
|
|
404
|
-
if preferred and self._device_key(device) == self.pointer_device_key:
|
|
405
|
-
continue
|
|
406
|
-
if not self.lib.ei_device_has_capability(device, EI_CAP_POINTER_ABSOLUTE):
|
|
407
|
-
continue
|
|
408
|
-
candidates.append(device)
|
|
409
|
-
converted = self._absolute_motion_for_device(device, x, y)
|
|
410
|
-
if converted is not None:
|
|
411
|
-
return device, converted[0], converted[1]
|
|
412
|
-
if candidates:
|
|
413
|
-
raise RuntimeError(f"no absolute-pointer EIS region contains desktop coordinate ({x}, {y})")
|
|
414
|
-
raise RuntimeError("no resumed EIS device with absolute-pointer capability")
|
|
415
|
-
|
|
416
|
-
def device_for_pointer_context(self, cap):
|
|
417
|
-
state = self.devices.get(self.pointer_device_key)
|
|
418
|
-
if (
|
|
419
|
-
state
|
|
420
|
-
and state["resumed"]
|
|
421
|
-
and state["emulating"]
|
|
422
|
-
and self.lib.ei_device_has_capability(state["ptr"], cap)
|
|
423
|
-
):
|
|
424
|
-
return state["ptr"]
|
|
425
|
-
if self.pointer_position is not None:
|
|
426
|
-
x, y = self.pointer_position
|
|
427
|
-
for candidate in self.devices.values():
|
|
428
|
-
if not candidate["resumed"] or not candidate["emulating"]:
|
|
429
|
-
continue
|
|
430
|
-
device = candidate["ptr"]
|
|
431
|
-
if not self.lib.ei_device_has_capability(device, cap):
|
|
432
|
-
continue
|
|
433
|
-
if self.lib.ei_device_get_region_at(device, float(x), float(y)):
|
|
434
|
-
return device
|
|
435
|
-
return self.device_for(cap)
|
|
436
|
-
|
|
437
|
-
def remember_pointer(self, device, position=None):
|
|
438
|
-
self.pointer_device_key = self._device_key(device)
|
|
439
|
-
self.pointer_position = position
|
|
440
|
-
|
|
441
|
-
def frame(self, device):
|
|
442
|
-
self.lib.ei_device_frame(device, self.lib.ei_now(self.ei))
|
|
443
|
-
|
|
444
|
-
def command(self, payload):
|
|
445
|
-
op = str(payload.get("op") or "")
|
|
446
|
-
if op == "key":
|
|
447
|
-
device = self.device_for(EI_CAP_KEYBOARD)
|
|
448
|
-
self.lib.ei_device_keyboard_key(device, int(payload["keycode"]), bool(payload.get("pressed")))
|
|
449
|
-
self.frame(device)
|
|
450
|
-
elif op == "motion":
|
|
451
|
-
device = self.device_for(EI_CAP_POINTER)
|
|
452
|
-
self.lib.ei_device_pointer_motion(device, float(payload.get("dx", 0)), float(payload.get("dy", 0)))
|
|
453
|
-
self.remember_pointer(device)
|
|
454
|
-
self.frame(device)
|
|
455
|
-
elif op == "motion_absolute":
|
|
456
|
-
x = float(payload["x"])
|
|
457
|
-
y = float(payload["y"])
|
|
458
|
-
device, motion_x, motion_y = self.device_for_absolute(x, y)
|
|
459
|
-
self.lib.ei_device_pointer_motion_absolute(device, motion_x, motion_y)
|
|
460
|
-
self.remember_pointer(device, (x, y))
|
|
461
|
-
self.frame(device)
|
|
462
|
-
elif op == "button":
|
|
463
|
-
button = int(payload["button"])
|
|
464
|
-
pressed = bool(payload.get("pressed"))
|
|
465
|
-
if pressed:
|
|
466
|
-
device = self.device_for_pointer_context(EI_CAP_BUTTON)
|
|
467
|
-
self.button_device_keys[button] = self._device_key(device)
|
|
468
|
-
else:
|
|
469
|
-
key = self.button_device_keys.pop(button, None)
|
|
470
|
-
state = self.devices.get(key) if key else None
|
|
471
|
-
if (
|
|
472
|
-
state
|
|
473
|
-
and state["resumed"]
|
|
474
|
-
and state["emulating"]
|
|
475
|
-
and self.lib.ei_device_has_capability(state["ptr"], EI_CAP_BUTTON)
|
|
476
|
-
):
|
|
477
|
-
device = state["ptr"]
|
|
478
|
-
else:
|
|
479
|
-
device = self.device_for_pointer_context(EI_CAP_BUTTON)
|
|
480
|
-
self.lib.ei_device_button_button(device, button, pressed)
|
|
481
|
-
self.frame(device)
|
|
482
|
-
elif op == "scroll":
|
|
483
|
-
device = self.device_for_pointer_context(EI_CAP_SCROLL)
|
|
484
|
-
self.lib.ei_device_scroll_discrete(device, int(payload.get("dx", 0)), int(payload.get("dy", 0)))
|
|
485
|
-
self.frame(device)
|
|
486
|
-
elif op == "ping":
|
|
487
|
-
pass
|
|
488
|
-
elif op == "close":
|
|
489
|
-
return False
|
|
490
|
-
else:
|
|
491
|
-
raise ValueError(f"unsupported EIS operation: {op}")
|
|
492
|
-
return True
|
|
493
|
-
|
|
494
|
-
def close(self):
|
|
495
|
-
for state in list(self.devices.values()):
|
|
496
|
-
if state["emulating"]:
|
|
497
|
-
try:
|
|
498
|
-
self.lib.ei_device_stop_emulating(state["ptr"])
|
|
499
|
-
except Exception:
|
|
500
|
-
pass
|
|
501
|
-
try:
|
|
502
|
-
self.lib.ei_device_unref(state["ptr"])
|
|
503
|
-
except Exception:
|
|
504
|
-
pass
|
|
505
|
-
self.devices.clear()
|
|
506
|
-
self.button_device_keys.clear()
|
|
507
|
-
if self.ei:
|
|
508
|
-
self.lib.ei_unref(self.ei)
|
|
509
|
-
self.ei = None
|
|
510
|
-
|
|
511
|
-
|
|
512
175
|
def main():
|
|
513
176
|
timeout_ms = max(1000, min(120000, int(os.environ.get("REMCP_EIS_TIMEOUT_MS", "30000"))))
|
|
514
177
|
restore_token = os.environ.get("REMCP_EIS_RESTORE_TOKEN", "")
|