@scrypted/server 0.119.1 → 0.119.3

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.
@@ -17,6 +17,12 @@ class ChildProcessWorker extends ws_1.EventEmitter {
17
17
  this.worker.on('disconnect', () => this.emit('error', new Error('disconnect')));
18
18
  this.worker.on('exit', (code, signal) => this.emit('exit', code, signal));
19
19
  this.worker.on('error', e => this.emit('error', e));
20
+ // aggressively catch errors
21
+ // ECONNRESET can be raised when the child process is killed
22
+ for (const stdio of this.worker.stdio || []) {
23
+ if (stdio)
24
+ stdio.on('error', e => this.emit('error', e));
25
+ }
20
26
  }
21
27
  get pid() {
22
28
  return this.worker?.pid;
@@ -1 +1 @@
1
- {"version":3,"file":"child-process-worker.js","sourceRoot":"","sources":["../../../src/plugin/runtime/child-process-worker.ts"],"names":[],"mappings":";;;AACA,2BAAkC;AAIlC,MAAsB,kBAAmB,SAAQ,iBAAY;IAOtC;IANT,MAAM,CAA6B;IAE7C,IAAI,YAAY;QACZ,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAED,YAAmB,QAAgB,EAAE,OAA6B;QAC9D,KAAK,EAAE,CAAC;QADO,aAAQ,GAAR,QAAQ,CAAQ;IAEnC,CAAC;IAED,WAAW;QACP,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAmB,EAAE,MAA6B,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAClH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,IAAI,GAAG;QACH,OAAO,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;IAC5B,CAAC;IAED,IAAI,MAAM;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,IAAI,MAAM;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,IAAI;QACA,IAAI,CAAC,IAAI,CAAC,MAAM;YACZ,OAAO;QACX,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;IAC5B,CAAC;CAIJ;AAvCD,gDAuCC"}
1
+ {"version":3,"file":"child-process-worker.js","sourceRoot":"","sources":["../../../src/plugin/runtime/child-process-worker.ts"],"names":[],"mappings":";;;AACA,2BAAkC;AAIlC,MAAsB,kBAAmB,SAAQ,iBAAY;IAOtC;IANT,MAAM,CAA6B;IAE7C,IAAI,YAAY;QACZ,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAED,YAAmB,QAAgB,EAAE,OAA6B;QAC9D,KAAK,EAAE,CAAC;QADO,aAAQ,GAAR,QAAQ,CAAQ;IAEnC,CAAC;IAED,WAAW;QACP,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAmB,EAAE,MAA6B,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAClH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAChF,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACpD,4BAA4B;QAC5B,4DAA4D;QAC5D,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;YAC1C,IAAI,KAAK;gBACL,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACtD,CAAC;IACL,CAAC;IAED,IAAI,GAAG;QACH,OAAO,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;IAC5B,CAAC;IAED,IAAI,MAAM;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,IAAI,MAAM;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,IAAI;QACA,IAAI,CAAC,IAAI,CAAC,MAAM;YACZ,OAAO;QACX,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;IAC5B,CAAC;CAIJ;AA7CD,gDA6CC"}
package/dist/rpc.d.ts CHANGED
@@ -94,7 +94,7 @@ export declare class RpcPeer {
94
94
  killed: Promise<string>;
95
95
  killedDeferred: Deferred;
96
96
  tags: any;
97
- yieldedAsyncIterators: Set<AsyncGenerator<unknown, any, unknown>>;
97
+ yieldedAsyncIterators: Set<AsyncGenerator<unknown, any, any>>;
98
98
  static readonly finalizerIdSymbol: unique symbol;
99
99
  static remotesCollected: number;
100
100
  static remotesCreated: number;
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@scrypted/server",
3
- "version": "0.119.1",
3
+ "version": "0.119.3",
4
4
  "description": "",
5
5
  "dependencies": {
6
6
  "@scrypted/ffmpeg-static": "^6.1.0-build1",
7
7
  "@scrypted/node-pty": "^1.0.18",
8
- "@scrypted/types": "^0.3.57",
8
+ "@scrypted/types": "^0.3.62",
9
9
  "adm-zip": "^0.5.16",
10
- "body-parser": "^1.20.2",
11
- "cookie-parser": "^1.4.6",
10
+ "body-parser": "^1.20.3",
11
+ "cookie-parser": "^1.4.7",
12
12
  "dotenv": "^16.4.5",
13
- "engine.io": "^6.6.0",
14
- "express": "^4.19.2",
13
+ "engine.io": "^6.6.2",
14
+ "express": "^4.21.1",
15
15
  "follow-redirects": "^1.15.9",
16
16
  "http-auth": "^4.2.0",
17
17
  "level": "^8.0.1",
@@ -19,24 +19,24 @@
19
19
  "node-dijkstra": "^2.5.0",
20
20
  "node-forge": "^1.3.1",
21
21
  "node-gyp": "^10.2.0",
22
- "py": "npm:@bjia56/portable-python@^0.1.83",
22
+ "py": "npm:@bjia56/portable-python@^0.1.94",
23
23
  "semver": "^7.6.3",
24
24
  "sharp": "^0.33.5",
25
25
  "source-map-support": "^0.5.21",
26
26
  "tar": "^7.4.3",
27
- "tslib": "^2.7.0",
28
- "typescript": "^5.5.4",
27
+ "tslib": "^2.8.0",
28
+ "typescript": "^5.6.3",
29
29
  "whatwg-mimetype": "^4.0.0",
30
30
  "ws": "^8.18.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/adm-zip": "^0.5.5",
34
34
  "@types/cookie-parser": "^1.4.7",
35
- "@types/express": "^4.17.21",
35
+ "@types/express": "^5.0.0",
36
36
  "@types/follow-redirects": "^1.14.4",
37
37
  "@types/http-auth": "^4.1.4",
38
- "@types/lodash": "^4.17.7",
39
- "@types/node": "^22.5.4",
38
+ "@types/lodash": "^4.17.10",
39
+ "@types/node": "^22.7.6",
40
40
  "@types/node-dijkstra": "^2.5.6",
41
41
  "@types/node-forge": "^1.3.11",
42
42
  "@types/semver": "^7.5.8",
@@ -4,10 +4,12 @@ import asyncio
4
4
  import base64
5
5
  import gc
6
6
  import hashlib
7
+ import inspect
7
8
  import multiprocessing
8
9
  import multiprocessing.connection
9
10
  import os
10
11
  import platform
12
+ import random
11
13
  import sys
12
14
  import threading
13
15
  import time
@@ -18,7 +20,7 @@ from asyncio.futures import Future
18
20
  from asyncio.streams import StreamReader, StreamWriter
19
21
  from collections.abc import Mapping
20
22
  from io import StringIO
21
- from typing import Any, Optional, Set, Tuple, TypedDict
23
+ from typing import Any, Optional, Set, Tuple, TypedDict, Callable, Coroutine
22
24
 
23
25
  import plugin_volume as pv
24
26
  import rpc
@@ -57,6 +59,14 @@ class SystemDeviceState(TypedDict):
57
59
  value: any
58
60
 
59
61
 
62
+ def ensure_not_coroutine(fn: Callable | Coroutine) -> Callable:
63
+ if inspect.iscoroutinefunction(fn):
64
+ def wrapper(*args, **kwargs):
65
+ return asyncio.create_task(fn(*args, **kwargs))
66
+ return wrapper
67
+ return fn
68
+
69
+
60
70
  class DeviceProxy(object):
61
71
  def __init__(self, systemManager: SystemManager, id: str):
62
72
  self.systemManager = systemManager
@@ -97,6 +107,116 @@ class DeviceProxy(object):
97
107
  return apply()
98
108
 
99
109
 
110
+ class EventListenerRegisterImpl(scrypted_python.scrypted_sdk.EventListenerRegister):
111
+ removeListener: Callable[[], None]
112
+
113
+ def __init__(self, removeListener: Callable[[], None] | Coroutine[Any, None, None]) -> None:
114
+ self.removeListener = ensure_not_coroutine(removeListener)
115
+
116
+
117
+ class EventRegistry(object):
118
+ systemListeners: Set[scrypted_python.scrypted_sdk.EventListener]
119
+ listeners: Mapping[str, Set[Callable[[scrypted_python.scrypted_sdk.EventDetails, Any], None]]]
120
+
121
+ __allowedEventInterfaces = set([
122
+ ScryptedInterface.ScryptedDevice.value,
123
+ 'Logger',
124
+ 'Storage'
125
+ ])
126
+
127
+ def __init__(self) -> None:
128
+ self.systemListeners = set()
129
+ self.listeners = {}
130
+
131
+ def __getMixinEventName(self, options: str | scrypted_python.scrypted_sdk.EventListenerOptions) -> str:
132
+ mixinId = None
133
+ if type(options) == str:
134
+ event = options
135
+ else:
136
+ options = options or {}
137
+ event = options.get("event", None)
138
+ mixinId = options.get("mixinId", None)
139
+ if not event:
140
+ event = "undefined"
141
+ if not mixinId:
142
+ return event
143
+ return f"{event}-mixin-{mixinId}"
144
+
145
+ def __generateBase36Str(self) -> str:
146
+ alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
147
+ return "".join(random.choices(alphabet, k=10))
148
+
149
+ def listen(
150
+ self, callback: scrypted_python.scrypted_sdk.EventListener
151
+ ) -> scrypted_python.scrypted_sdk.EventListenerRegister:
152
+ callback = ensure_not_coroutine(callback)
153
+ self.systemListeners.add(callback)
154
+ return EventListenerRegisterImpl(lambda: self.systemListeners.remove(callback))
155
+
156
+ def listenDevice(
157
+ self,
158
+ id: str,
159
+ options: str | scrypted_python.scrypted_sdk.EventListenerOptions,
160
+ callback: Callable[[scrypted_python.scrypted_sdk.EventDetails, Any], None],
161
+ ) -> scrypted_python.scrypted_sdk.EventListenerRegister:
162
+ event = self.__getMixinEventName(options)
163
+ token = f"{id}#{event}"
164
+ events = self.listeners.get(token)
165
+ if not events:
166
+ events = set()
167
+ self.listeners[token] = events
168
+ callback = ensure_not_coroutine(callback)
169
+ self.listeners[id].add(callback)
170
+ return EventListenerRegisterImpl(lambda: self.listeners[id].remove(callback))
171
+
172
+ def notify(self, id: str, eventTime: int, eventInterface: str, property: str, value: Any, options: dict = None):
173
+ options = options or {}
174
+ changed = options.get("changed")
175
+ mixinId = options.get("mixinId")
176
+
177
+ # prevent property event noise
178
+ if property and not changed:
179
+ return False
180
+
181
+ eventDetails = {
182
+ "eventId": None,
183
+ "eventTime": eventTime,
184
+ "eventInterface": eventInterface,
185
+ "property": property,
186
+ "mixinId": mixinId,
187
+ }
188
+
189
+ return self.notifyEventDetails(id, eventDetails, value)
190
+
191
+ def notifyEventDetails(self, id: str, eventDetails: scrypted_python.scrypted_sdk.EventDetails, value: Any, eventInterface: str = None):
192
+ if not eventDetails.get("eventId"):
193
+ eventDetails["eventId"] = self.__generateBase36Str()
194
+ if not eventInterface:
195
+ eventInterface = eventDetails.get("eventInterface")
196
+
197
+ # system listeners only get state changes.
198
+ # there are many potentially noisy stateless events, like
199
+ # object detection and settings changes
200
+ if (eventDetails.get("property") and not eventDetails.get("mixinId")) or \
201
+ (eventInterface in EventRegistry.__allowedEventInterfaces):
202
+ for listener in self.systemListeners:
203
+ listener(id, eventDetails, value)
204
+
205
+ token = f"{id}#{eventInterface}"
206
+ listeners = self.listeners.get(token)
207
+ if listeners:
208
+ for listener in listeners:
209
+ listener(eventDetails, value)
210
+
211
+ token = f"{id}#undefined"
212
+ listeners = self.listeners.get(token)
213
+ if listeners:
214
+ for listener in listeners:
215
+ listener(eventDetails, value)
216
+
217
+ return True
218
+
219
+
100
220
  class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
101
221
  def __init__(
102
222
  self, api: Any, systemState: Mapping[str, Mapping[str, SystemDeviceState]]
@@ -105,6 +225,7 @@ class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
105
225
  self.api = api
106
226
  self.systemState = systemState
107
227
  self.deviceProxies: Mapping[str, DeviceProxy] = {}
228
+ self.events = EventRegistry()
108
229
 
109
230
  async def getComponent(self, id: str) -> Any:
110
231
  return await self.api.getComponent(id)
@@ -170,20 +291,34 @@ class SystemManager(scrypted_python.scrypted_sdk.types.SystemManager):
170
291
  if checkName.get("value", None) == name:
171
292
  return self.getDeviceById(check)
172
293
 
173
- # TODO
174
- async def listen(
294
+ def listen(
175
295
  self, callback: scrypted_python.scrypted_sdk.EventListener
176
296
  ) -> scrypted_python.scrypted_sdk.EventListenerRegister:
177
- return super().listen(callback)
297
+ return self.events.listen(callback)
178
298
 
179
- # TODO
180
- async def listenDevice(
299
+ def listenDevice(
181
300
  self,
182
301
  id: str,
183
- event: str | scrypted_python.scrypted_sdk.EventListenerOptions,
302
+ options: str | scrypted_python.scrypted_sdk.EventListenerOptions,
184
303
  callback: scrypted_python.scrypted_sdk.EventListener,
185
304
  ) -> scrypted_python.scrypted_sdk.EventListenerRegister:
186
- return super().listenDevice(id, event, callback)
305
+ callback = ensure_not_coroutine(callback)
306
+ if type(options) != str and options.get("watch"):
307
+ return self.events.listenDevice(
308
+ id, options,
309
+ lambda eventDetails, eventData: callback(self.getDeviceById(id), eventDetails, eventData)
310
+ )
311
+
312
+ register_fut = asyncio.ensure_future(
313
+ self.api.listenDevice(
314
+ id, options,
315
+ lambda eventDetails, eventData: callback(self.getDeviceById(id), eventDetails, eventData)
316
+ )
317
+ )
318
+ async def unregister():
319
+ register = await register_fut
320
+ await register.removeListener()
321
+ return EventListenerRegisterImpl(lambda: asyncio.ensure_future(unregister()))
187
322
 
188
323
  async def removeDevice(self, id: str) -> None:
189
324
  return await self.api.removeDevice(id)
@@ -19,6 +19,12 @@ export abstract class ChildProcessWorker extends EventEmitter implements Runtime
19
19
  this.worker.on('disconnect', () => this.emit('error', new Error('disconnect')));
20
20
  this.worker.on('exit', (code, signal) => this.emit('exit', code, signal));
21
21
  this.worker.on('error', e => this.emit('error', e));
22
+ // aggressively catch errors
23
+ // ECONNRESET can be raised when the child process is killed
24
+ for (const stdio of this.worker.stdio || []) {
25
+ if (stdio)
26
+ stdio.on('error', e => this.emit('error', e));
27
+ }
22
28
  }
23
29
 
24
30
  get pid() {