Compare commits

..

29 Commits

Author SHA1 Message Date
Koushik Dutta
e33cacd15d postbeta 2024-09-06 10:54:31 -07:00
Koushik Dutta
e225b746c4 server: update deps 2024-09-06 10:54:24 -07:00
Koushik Dutta
1e1fda6b9a server: findPluginDevice is not optional 2024-09-06 10:50:06 -07:00
Koushik Dutta
e23c9c22dc onnx: fix platform check 2024-09-05 10:34:22 -07:00
Koushik Dutta
28f1ce9d4e postbeta 2024-09-04 12:19:33 -07:00
Koushik Dutta
5f4e2793ff server: possibly fix bug where rpc object may not be found 2024-09-04 10:36:56 -07:00
Koushik Dutta
1e1755fa7e server: cluster cleanups 2024-09-04 10:17:30 -07:00
Koushik Dutta
ebd56b86e4 postbeta 2024-09-04 09:30:27 -07:00
Koushik Dutta
4607bec07c server: improve fork/thread logging 2024-09-04 09:30:19 -07:00
Koushik Dutta
a26cdfea2a postbeta 2024-09-03 22:45:04 -07:00
Koushik Dutta
6d0da449ad server: simplify convoluted peer key 2024-09-03 22:31:51 -07:00
Koushik Dutta
ad15fe3324 cloud: update deps 2024-09-03 17:20:39 -07:00
Koushik Dutta
fbe3e83884 cameras: move autoconfiguration button so its less accidentally clickable 2024-09-03 16:01:11 -07:00
Koushik Dutta
0d4cf34930 reolink: move some device creation stuff into advanced tab 2024-09-03 15:54:36 -07:00
Koushik Dutta
3a3e15cd74 server: allow main/child thread ipc 2024-09-03 15:26:21 -07:00
Koushik Dutta
8b9cfebbfa cloud: handle mux carrier errors 2024-09-03 08:43:06 -07:00
Koushik Dutta
b3087058a7 google-home/alexa: republish with new sdk for media converter 2024-09-02 08:40:03 -07:00
Koushik Dutta
4e923a78da cloud: fix short lived regression 2024-09-01 20:42:32 -07:00
Koushik Dutta
b5593d6251 cloud: cleanups 2024-09-01 20:14:47 -07:00
Koushik Dutta
40b7b621a0 openvino: update 2024-09-01 08:17:54 -07:00
Koushik Dutta
7104ad6378 openvino: improve device selection 2024-08-29 21:15:19 -07:00
Koushik Dutta
51f893ef63 install: update proxmox and ha 2024-08-29 20:58:54 -07:00
Koushik Dutta
29c5dfd73b core: install npu drivers in lxc 2024-08-29 20:37:42 -07:00
Koushik Dutta
1615f79a0b install: npu driver update 2024-08-29 20:32:35 -07:00
Koushik Dutta
dc5a6126b9 sdk: add id to recorded event on notifications 2024-08-29 08:02:33 -07:00
Koushik Dutta
0fbe3e4686 cloud: upnp no longer default 2024-08-28 13:23:52 -07:00
Koushik Dutta
c9641568f8 core: publish 2024-08-27 21:54:36 -07:00
Koushik Dutta
dceea38eb8 core: publish 2024-08-27 21:31:22 -07:00
Koushik Dutta
cd1fce71e2 postrelease 2024-08-27 21:15:09 -07:00
53 changed files with 423 additions and 295 deletions

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "v0.114.0-jammy-full"
version: "v0.116.0-jammy-full"
slug: scrypted
description: Scrypted is a high performance home video integration and automation platform
url: "https://github.com/koush/scrypted"

View File

@@ -33,20 +33,20 @@ rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
# different npu downloads for ubuntu versions
if [ -n "$UBUNTU_22_04" ]
then
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-driver-compiler-npu_1.5.1.20240708-9842236399_ubuntu22.04_amd64.deb
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-driver-compiler-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
# firmware can only be installed on host. will cause problems inside container.
if [ -n "$INTEL_FW_NPU" ]
then
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-fw-npu_1.5.1.20240708-9842236399_ubuntu22.04_amd64.deb
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
fi
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-level-zero-npu_1.5.1.20240708-9842236399_ubuntu22.04_amd64.deb
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
else
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-driver-compiler-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
if [ -n "$INTEL_FW_NPU" ]
then
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-fw-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
fi
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-level-zero-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
fi
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero_1.17.6+u22.04_amd64.deb

View File

@@ -10,7 +10,7 @@ function readyn() {
}
cd /tmp
SCRYPTED_VERSION=v0.96.0
SCRYPTED_VERSION=v0.116.0
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
if [ -z "$VMID" ]
then

View File

@@ -1,6 +1,11 @@
<details>
<summary>Changelog</summary>
### 0.3.2
alexa: fix syncedDevices being undefined
### 0.3.1
alexa/google-home: fix potential vulnerability. do not allow local network control using cloud tokens belonging to a different user. the plugins are now locked to a specific scrypted cloud account once paired.

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/alexa",
"version": "0.3.2",
"version": "0.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/alexa",
"version": "0.3.2",
"version": "0.3.3",
"dependencies": {
"axios": "^1.3.4",
"uuid": "^9.0.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.3.2",
"version": "0.3.3",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.161",
"version": "0.0.162",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.161",
"version": "0.0.162",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.161",
"version": "0.0.162",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -8,6 +8,7 @@ import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { createRtspMediaStreamOptions, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { AmcrestCameraClient, AmcrestEvent, AmcrestEventData } from "./amcrest-api";
import { amcrestAutoConfigureSettings, autoconfigureSettings } from "./amcrest-configure";
import { group } from "console";
const { mediaManager } = sdk;
@@ -336,10 +337,14 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
const ac = {
...automaticallyConfigureSettings,
subgroup: 'Advanced',
};
ac.type = 'button';
ret.push(ac);
ret.push({ ...amcrestAutoConfigureSettings });
ret.push({
...amcrestAutoConfigureSettings,
subgroup: 'Advanced',
});
return ret;
}

View File

@@ -16,7 +16,6 @@
"ts-node/register"
],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
},
{

View File

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "127.0.0.1",
"scrypted.debugHost": "scrypted-nvr",
}

View File

@@ -1,14 +1,14 @@
{
"name": "@scrypted/cloud",
"version": "0.2.32",
"version": "0.2.37",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/cloud",
"version": "0.2.32",
"version": "0.2.37",
"dependencies": {
"@eneris/push-receiver": "^4.1.6",
"@eneris/push-receiver": "^4.2.0",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"bpmux": "^8.2.1",
@@ -21,7 +21,7 @@
"@types/http-proxy": "^1.17.15",
"@types/ip": "^1.1.3",
"@types/nat-upnp": "^1.1.5",
"@types/node": "^22.1.0",
"@types/node": "^22.5.2",
"ts-node": "^10.9.2"
}
},
@@ -79,7 +79,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.50",
"version": "0.3.61",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.24.7",
@@ -2416,14 +2416,14 @@
}
},
"node_modules/@eneris/push-receiver": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-4.1.6.tgz",
"integrity": "sha512-M5EZK7s3AujCuWDrqbEHeVRyyeK145hs1FtWkrOKtEbD0lSZgLfhoHIr09adc+hvcNDOAy8awVh/64XsprWVLw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-4.2.0.tgz",
"integrity": "sha512-OGc4dUcy9yvKTIShOpBVHsYGEC7qRo6JNRRlsL7kaa3/uJSJyRoLOUdKsTqH8q76qldViXb8mEdOzCGv8wwK4A==",
"dependencies": {
"http_ece": "^1.2.1",
"jsonwebtoken": "^9.0.2",
"long": "^5.2.3",
"protobufjs": "^7.3.2"
"protobufjs": "^7.3.3"
},
"engines": {
"node": ">=20"
@@ -2568,11 +2568,11 @@
}
},
"node_modules/@types/node": {
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz",
"integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==",
"version": "22.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz",
"integrity": "sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==",
"dependencies": {
"undici-types": "~6.13.0"
"undici-types": "~6.19.2"
}
},
"node_modules/acorn": {
@@ -2813,9 +2813,9 @@
"link": true
},
"node_modules/protobufjs": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz",
"integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
@@ -2932,9 +2932,9 @@
}
},
"node_modules/undici-types": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
"integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg=="
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",

View File

@@ -30,15 +30,14 @@
"realfs": true,
"interfaces": [
"SystemSettings",
"BufferConverter",
"MediaConverter",
"OauthClient",
"Settings",
"DeviceProvider",
"HttpRequestHandler"
]
},
"dependencies": {
"@eneris/push-receiver": "^4.1.6",
"@eneris/push-receiver": "^4.2.0",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"bpmux": "^8.2.1",
@@ -51,8 +50,8 @@
"@types/http-proxy": "^1.17.15",
"@types/ip": "^1.1.3",
"@types/nat-upnp": "^1.1.5",
"@types/node": "^22.1.0",
"@types/node": "^22.5.2",
"ts-node": "^10.9.2"
},
"version": "0.2.32"
"version": "0.2.37"
}

View File

@@ -27,7 +27,7 @@ function runLog(bin: string, args: string[]) {
return cp;
}
async function runLogWait(bin: string, args: string[], timeout: number, signal?: AbortSignal) {
async function runLogWait(bin: string, args: string[], timeout: number, signal?: AbortSignal, outputChanged?: (output: string) => void) {
const cp = runLog(bin, args);
signal?.addEventListener('abort', () => {
@@ -37,9 +37,11 @@ async function runLogWait(bin: string, args: string[], timeout: number, signal?:
let output: string = '';
cp.stdio[1].on('data', (data) => {
output += data.toString();
outputChanged?.(output);
});
cp.stdio[2].on('data', (data) => {
output += data.toString();
outputChanged?.(output);
});
await timeoutPromise(timeout, once(cp, 'exit'));
@@ -49,15 +51,23 @@ async function runLogWait(bin: string, args: string[], timeout: number, signal?:
return output;
}
async function login(bin: string, signal?: AbortSignal) {
async function login(bin: string, signal?: AbortSignal, urlCallback?: (url: string) => void) {
const userHome = process.env.HOME || process.env.USERPROFILE;
const certPem = path.join(userHome, '.cloudflared', 'cert.pem');
rmSync(certPem, { force: true, recursive: true });
await runLogWait(bin, ['tunnel', 'login'], 300000, signal);
await runLogWait(bin, ['tunnel', 'login'], 300000, signal, output => {
const match = output.match(/Please open the following URL and log in with your Cloudflare account:(?<url>.*?)Leave/s);
if (match) {
const url = match.groups.url.trim();
if (url)
urlCallback(url);
}
});
}
async function createTunnel(bin: string, domain: string) {
await runLogWait(bin, ['tunnel', 'cleanup', domain], 30000).catch(() => { });
await runLogWait(bin, ['tunnel', 'delete', domain], 30000).catch(() => { });
return runLogWait(bin, ['tunnel', 'create', domain], 30000);
}
@@ -103,10 +113,10 @@ async function ensureBin(bin: string) {
return bin;
}
export async function createLocallyManagedTunnel(domain: string, bin?: string, signal?: AbortSignal) {
export async function createLocallyManagedTunnel(domain: string, bin?: string, signal?: AbortSignal, urlCallback?: (url: string) => void) {
bin = await ensureBin(bin);
await login(bin, signal);
await login(bin, signal, urlCallback);
const createOutput = await createTunnel(bin, domain);
const jsonFilePath = extractJsonFilePath(createOutput);

View File

@@ -1,7 +1,8 @@
import { Deferred } from "@scrypted/common/src/deferred";
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MediaConverter, MediaObject, MediaObjectOptions, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import bpmux from 'bpmux';
import { ChildProcess } from "child_process";
import * as cloudflared from 'cloudflared';
import crypto from 'crypto';
import { once } from 'events';
@@ -20,12 +21,9 @@ import { sleep } from '../../../common/src/sleep';
import { createSelfSignedCertificate } from '../../../server/src/cert';
import { httpFetch } from '../../../server/src/fetch/http-fetch';
import { installCloudflared } from "./cloudflared-install";
import { createLocallyManagedTunnel, runLocallyManagedTunnel } from "./cloudflared-local-managed";
import { PushManager } from './push';
import { qsparse, qsstringify } from "./qs";
import { createLocallyManagedTunnel, runLocallyManagedTunnel } from "./cloudflared-local-managed";
import { ChildProcess } from "child_process";
// import { registerDuckDns } from "./greenlock";
const { deviceManager, endpointManager, systemManager } = sdk;
@@ -34,25 +32,7 @@ const SCRYPTED_SERVER = localStorage.getItem('scrypted-server') || 'home.scrypte
const SCRYPTED_CLOUD_MESSAGE_PATH = '/_punch/cloudmessage';
class ScryptedPush extends ScryptedDeviceBase implements BufferConverter {
constructor(public cloud: ScryptedCloud) {
super('push');
this.fromMimeType = ScryptedMimeTypes.PushEndpoint;
this.toMimeType = ScryptedMimeTypes.Url;
}
async convert(data: Buffer | string, fromMimeType: string): Promise<Buffer> {
const validDomain = this.cloud.getSSLHostname();
if (validDomain)
return Buffer.from(`https://${validDomain}${await this.cloud.getCloudMessagePath()}/${data}`);
const url = `http://127.0.0.1/push/${data}`;
return this.cloud.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.cloud.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
}
}
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, BufferConverter, DeviceProvider, HttpRequestHandler {
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, MediaConverter, HttpRequestHandler {
cloudflareTunnel: string;
cloudflared: {
url: Promise<string>;
@@ -62,7 +42,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
server: http.Server;
secureServer: https.Server;
proxy: HttpProxy;
push: ScryptedPush;
whitelisted = new Map<string, string>();
reregisterTimer: NodeJS.Timeout;
storageSettings = new StorageSettings(this, {
@@ -85,15 +64,16 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
persistedDefaultValue: crypto.randomBytes(8).toString('hex'),
},
forwardingMode: {
title: "Port Forwarding Mode",
description: "The port forwarding mode used to expose the HTTPS port. If port forwarding is disabled or unavailable, Scrypted Cloud will fall back to push to initiate connections with this Scrypted server. Port Forwarding and UPNP are optional but will significantly speed up cloud connections.",
title: "Connection Mode",
description: "The connection mode that exposes this server to the internet.",
choices: [
"Default",
"UPNP",
"Router Forward",
"Custom Domain",
"Disabled",
],
defaultValue: 'UPNP',
defaultValue: 'Default',
onPut: () => this.scheduleRefreshPortForward(),
},
hostname: {
@@ -189,10 +169,27 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
title: 'Cloudflare Tunnel Custom Domain',
placeholder: 'scrypted.example.com',
description: 'Optional: Host a custom domain with Cloudflare. After setting the domain, complete the Cloudflare browser login link shown in Scrypted Cloud Plugin Console.',
mapPut: (ov, nv) => {
try {
const url = new URL(nv);
return url.hostname;
}
catch (e) {
return nv;
}
},
onPut: (_, nv) => {
if (!nv)
this.storageSettings.values.cloudflaredTunnelCredentials = undefined;
this.doCloudflaredLogin(nv);
},
},
cloudflaredTunnelLoginUrl: {
group: 'Cloudflare',
type: 'html',
title: 'Cloudflare Tunnel Login',
hide: true,
},
cloudflaredTunnelUrl: {
group: 'Cloudflare',
title: 'Cloudflare Tunnel URL',
@@ -245,6 +242,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
reverseConnections = new Set<Duplex>();
cloudflaredLoginController?: AbortController;
get portForwardingDisabled() {
return this.storageSettings.values.forwardingMode === 'Disabled' || this.storageSettings.values.forwardingMode === 'Default';
}
get cloudflareTunnelHost() {
if (!this.cloudflareTunnel)
return;
@@ -254,6 +255,17 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
constructor() {
super();
this.converters = [
[ScryptedMimeTypes.LocalUrl, ScryptedMimeTypes.Url],
[ScryptedMimeTypes.PushEndpoint, ScryptedMimeTypes.Url],
];
// legacy cleanup
this.fromMimeType = undefined;
this.toMimeType = undefined;
deviceManager.onDevicesChanged({
devices: [],
});
this.storageSettings.settings.register.onPut = async () => {
await this.sendRegistrationId(await this.manager.registrationId);
}
@@ -281,8 +293,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
};
this.storageSettings.settings.securePort.onGet = async () => {
const hide = this.portForwardingDisabled;
return {
hide: this.storageSettings.values.forwardingMode === 'Disabled',
hide,
}
};
@@ -292,20 +305,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
};
// this.storageSettings.settings.duckDnsToken.onGet = async () => {
// return {
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
// || this.storageSettings.values.forwardingMode === 'Disabled',
// }
// };
// this.storageSettings.settings.duckDnsHostname.onGet = async () => {
// return {
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
// || this.storageSettings.values.forwardingMode === 'Disabled',
// }
// };
this.storageSettings.settings.cloudflaredTunnelCustomDomain.onGet =
this.storageSettings.settings.cloudflaredTunnelUrl.onGet = async () => {
return {
@@ -313,21 +312,21 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
};
this.log.clearAlerts();
this.storageSettings.settings.securePort.onPut = (ov, nv) => {
if (ov && ov !== nv)
this.log.a('Reload the Scrypted Cloud Plugin to apply the port change.');
};
this.fromMimeType = ScryptedMimeTypes.LocalUrl;
this.toMimeType = ScryptedMimeTypes.Url;
if (!this.storageSettings.values.certificate)
this.storageSettings.values.certificate = createSelfSignedCertificate();
if (this.storageSettings.values.cloudflaredTunnelCustomDomain && !this.storageSettings.values.cloudflaredTunnelCredentials)
this.storageSettings.values.cloudflaredTunnelCustomDomain = undefined;
this.log.clearAlerts();
const proxy = this.setupProxyServer();
this.setupCloudPush();
this.updateCors();
const observeRegistrations = () => {
@@ -445,7 +444,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
async testPortForward() {
try {
if (this.storageSettings.values.forwardingMode === 'Disabled')
if (this.portForwardingDisabled)
throw new Error('Port forwarding is disabled.');
const pluginPath = await endpointManager.getPath(undefined, {
@@ -480,7 +479,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
if (!upnpPort)
upnpPort = Math.round(Math.random() * 20000 + 40000);
if (this.storageSettings.values.forwardingMode === 'Disabled') {
if (this.portForwardingDisabled) {
this.updatePortForward(upnpPort);
return;
}
@@ -602,10 +601,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
getAuthority() {
const { forwardingMode } = this.storageSettings.values;
if (forwardingMode === 'Disabled')
if (this.portForwardingDisabled)
return {};
const { forwardingMode } = this.storageSettings.values;
const upnp_port = forwardingMode === 'Custom Domain' ? 443 : this.storageSettings.values.upnpPort;
const hostname = forwardingMode === 'Custom Domain'
? this.storageSettings.values.hostname
@@ -687,18 +687,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
}
async setupCloudPush() {
await deviceManager.onDeviceDiscovered(
{
name: 'Cloud Push Endpoint',
type: ScryptedDeviceType.API,
nativeId: 'push',
interfaces: [ScryptedInterface.BufferConverter],
},
);
this.push = new ScryptedPush(this);
}
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
if (request.url.endsWith('/testPortForward')) {
response.send(this.randomBytes);
@@ -725,10 +713,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
}
async getDevice(nativeId: string) {
return this.push;
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
@@ -744,19 +728,34 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
return this.getSSLHostname() || SCRYPTED_SERVER;
}
async convert(data: Buffer, fromMimeType: string, toMimeType: string): Promise<Buffer> {
// if cloudflare is enabled and the plugin isn't set up as a custom domain, try to use the cloudflare url for
// short lived urls.
if (this.cloudflareTunnel && this.storageSettings.values.forwardingMode !== 'Custom Domain') {
const params = new URLSearchParams(toMimeType.split(';')[1] || '');
if (params.get('short-lived') === 'true') {
const u = new URL(data.toString(), this.cloudflareTunnel);
u.host = this.cloudflareTunnelHost;
u.port = '';
return Buffer.from(u.toString());
async convertMedia(data: string | Buffer | any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<MediaObject | Buffer | any> {
if (!toMimeType.startsWith(ScryptedMimeTypes.Url))
throw new Error('unsupported cloud url conversion');
if (fromMimeType.startsWith(ScryptedMimeTypes.LocalUrl)) {
// if cloudflare is enabled and the plugin isn't set up as a custom domain, try to use the cloudflare url for
// short lived urls.
if (this.cloudflareTunnel && this.storageSettings.values.forwardingMode !== 'Custom Domain') {
const params = new URLSearchParams(toMimeType.split(';')[1] || '');
if (params.get('short-lived') === 'true') {
const u = new URL(data.toString(), this.cloudflareTunnel);
u.host = this.cloudflareTunnelHost;
u.port = '';
return Buffer.from(u.toString());
}
}
return this.whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}`);
}
return this.whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}`);
else if (fromMimeType.startsWith(ScryptedMimeTypes.PushEndpoint)) {
const validDomain = this.getSSLHostname();
if (validDomain)
return Buffer.from(`https://${validDomain}${await this.getCloudMessagePath()}/${data}`);
const url = `http://127.0.0.1/push/${data}`;
return this.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
}
throw new Error('unsupported cloud url conversion');
}
async getSettings(): Promise<Setting[]> {
@@ -961,6 +960,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
socket.pipe(local).pipe(socket);
});
mux.on('error', () => {
client.destroy();
});
}
});
@@ -1216,18 +1218,25 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
return;
}
this.log.a('Visit the URL printed in the Scrypted Cloud plugin console to log into Cloudflare.');
// this.log.a('Visit the URL printed in the Scrypted Cloud plugin console to log into Cloudflare.');
const customDomain = this.storageSettings.values.cloudflaredTunnelCustomDomain;
try {
this.cloudflaredLoginController?.abort();
this.cloudflaredLoginController = new AbortController();
const { bin } = await installCloudflared();
const jsonContents = await createLocallyManagedTunnel(domain, bin, this.cloudflaredLoginController.signal);
const jsonContents = await createLocallyManagedTunnel(domain, bin, this.cloudflaredLoginController.signal, url => {
this.console.warn('Cloudflare login URL:', url);
this.storageSettings.values.cloudflaredTunnelLoginUrl = `<div style="padding-bottom: 16px"><a href="${url}" target="_blank" >Click here to log into Cloudflare</a></div>`;
this.storageSettings.settings.cloudflaredTunnelLoginUrl.hide = false;
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
});
this.storageSettings.values.cloudflaredTunnelCredentials = jsonContents;
this.storageSettings.values.cloudflaredTunnelToken = undefined;
this.cloudflared?.child.kill();
}
catch (e) {
this.storageSettings.values.cloudflaredTunnelCustomDomain = undefined;
if (customDomain)
this.storageSettings.values.cloudflaredTunnelCustomDomain = undefined;
this.console.error('cloudflared login error', e);
this.log.a('Cloudflare login error. See console logs.');
}

View File

@@ -29,11 +29,14 @@ export class PushManager extends EventEmitter {
const instance = new PushReceiver({
...savedConfig,
firebase: {
messagingSenderId: senderId,
projectId: 'scrypted-app',
apiKey: 'AIzaSyDI0bgFuVPIqKZoNpB-iTOU7ijIeepxOXE',
appId: '1:827888101440:web:6ff9f8ada107e9cc0097a5',
} ,
apiKey: "AIzaSyDI0bgFuVPIqKZoNpB-iTOU7ijIeepxOXE",
authDomain: "scrypted-app.firebaseapp.com",
databaseURL: "https://scrypted-app.firebaseio.com",
projectId: "scrypted-app",
storageBucket: "scrypted-app.appspot.com",
messagingSenderId: "827888101440",
appId: "1:827888101440:web:6ff9f8ada107e9cc0097a5"
},
heartbeatIntervalMs: 15 * 60 * 1000,
});
@@ -57,7 +60,12 @@ export class PushManager extends EventEmitter {
this.emit('message', message.data);
});
await instance.connect();
try {
await instance.connect();
}
catch (e) {
console.error('failed to connect to push server', e);
}
return savedConfig.credentials?.fcm?.token || deferred.promise;
})();

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/core",
"version": "0.3.65",
"version": "0.3.68",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.3.65",
"version": "0.3.68",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.3.65",
"version": "0.3.68",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -48,7 +48,7 @@ export async function checkLxcDependencies() {
// the current workaround is to install the release manually.
// https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
const output = await new Promise<string>((r,f)=> child_process.exec("sh -c 'apt show versions intel-opencl-icd'", (err, stdout, stderr) => {
if (err)
if (err && !stdout && !stderr)
f(err);
else
r(stdout + '\n' + stderr);
@@ -73,6 +73,30 @@ export async function checkLxcDependencies() {
sdk.log.a('Failed to verify/install intel-opencl-icd version.');
}
try {
const output = await new Promise<string>((r,f)=> child_process.exec("sh -c 'apt show versions intel-driver-compiler-npu'", (err, stdout, stderr) => {
if (err && !stdout && !stderr)
f(err);
else
r(stdout + '\n' + stderr);
}));
if (
// apt
output.includes('No packages found')
) {
const cp = child_process.spawn('sh', ['-c', 'curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to install intel-driver-compiler-npu.');
else
needRestart = true;
}
}
catch (e) {
sdk.log.a('Failed to verify/install intel-driver-compiler-npu.');
}
if (needRestart)
sdk.log.a('A system update is pending. Please restart Scrypted to apply changes.');
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/google-home",
"version": "0.0.60",
"version": "0.0.61",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/google-home",
"version": "0.0.60",
"version": "0.0.61",
"dependencies": {
"@googleapis/homegraph": "^2.0.0",
"@homebridge/ciao": "^1.1.5",

View File

@@ -49,5 +49,5 @@
"@types/lodash": "^4.14.168",
"@types/url-parse": "^1.4.3"
},
"version": "0.0.60"
"version": "0.0.61"
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.159",
"version": "0.0.160",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/hikvision",
"version": "0.0.159",
"version": "0.0.160",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.159",
"version": "0.0.160",
"description": "Hikvision Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -465,10 +465,14 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
const ac = {
...automaticallyConfigureSettings,
subgroup: 'Advanced',
};
ac.type = 'button';
ret.push(ac);
ret.push({ ...hikvisionAutoConfigureSettings });
ret.push({
...hikvisionAutoConfigureSettings,
subgroup: 'Advanced',
});
return ret;
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/onnx",
"version": "0.1.109",
"version": "0.1.110",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/onnx",
"version": "0.1.109",
"version": "0.1.110",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -42,5 +42,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.109"
"version": "0.1.110"
}

View File

@@ -94,7 +94,7 @@ class ONNXPlugin(
if sys.platform == 'darwin':
providers.append("CoreMLExecutionProvider")
if ('linux' in sys.platform or 'win' in sys.platform) and platform.machine() == 'x86_64':
if ('linux' in sys.platform or 'win' in sys.platform) and (platform.machine() == 'x86_64' or platform.machine() == 'AMD64'):
deviceId = int(deviceId)
providers.append(("CUDAExecutionProvider", { "device_id": deviceId }))

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/onvif",
"version": "0.1.22",
"version": "0.1.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/onvif",
"version": "0.1.22",
"version": "0.1.23",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/onvif",
"version": "0.1.22",
"version": "0.1.23",
"description": "ONVIF Camera Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -227,9 +227,14 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
const ac = {
...automaticallyConfigureSettings,
subgroup: 'Advanced',
};
ac.type = 'button';
ret.push(ac);
ret.push({
...onvifAutoConfigureSettings,
subgroup: 'Advanced',
});
return ret;
}

View File

@@ -21,7 +21,7 @@
},
{
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
"remoteRoot": "."
},
]

View File

@@ -1,12 +1,12 @@
{
// docker installation
"scrypted.debugHost": "scrypted-demo",
"scrypted.serverRoot": "/server",
// "scrypted.debugHost": "scrypted-demo",
// "scrypted.serverRoot": "/server",
// proxmox installation
// "scrypted.debugHost": "scrypted-server",
// "scrypted.serverRoot": "/root/.scrypted",
"scrypted.debugHost": "scrypted-nvr",
"scrypted.serverRoot": "/root/.scrypted",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
@@ -18,7 +18,7 @@
// "scrypted.debugHost": "koushik-winvm",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/openvino",
"version": "0.1.109",
"version": "0.1.111",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/openvino",
"version": "0.1.109",
"version": "0.1.111",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -42,5 +42,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.109"
"version": "0.1.111"
}

View File

@@ -101,6 +101,8 @@ class OpenVINOPlugin(
nvidia = False
iris_xe = False
arc = False
npu = False
gpu = False
dgpus = []
# search for NVIDIA dGPU, as that is not preferred by AUTO for some reason?
@@ -116,6 +118,10 @@ class OpenVINOPlugin(
if "NVIDIA" in full_device_name and "dGPU" in full_device_name:
dgpus.append(device)
nvidia = True
if "NPU" in device:
npu = True
if "GPU" in device:
gpu = True
except:
pass
@@ -123,8 +129,15 @@ class OpenVINOPlugin(
if mode == "Default":
mode = "AUTO"
if len(dgpus):
if npu:
if gpu:
mode = f"AUTO:NPU,GPU,CPU"
else:
mode = f"AUTO:NPU,CPU"
elif len(dgpus):
mode = f"AUTO:{','.join(dgpus)},CPU"
elif gpu:
mode = f"GPU"
mode = mode or "AUTO"
self.mode = mode
@@ -137,7 +150,7 @@ class OpenVINOPlugin(
if model == "Default" or model not in availableModels:
if model != "Default":
self.storage.setItem("model", "Default")
if arc or nvidia:
if arc or nvidia or npu:
model = "scrypted_yolov9c_320"
elif iris_xe:
model = "scrypted_yolov9s_320"

View File

@@ -1,5 +1,5 @@
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
numpy==1.26.4
openvino==2024.2.0
openvino==2024.3.0
Pillow==10.3.0
opencv-python==4.10.0.84

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/reolink",
"version": "0.0.93",
"version": "0.0.94",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/reolink",
"version": "0.0.93",
"version": "0.0.94",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/reolink",
"version": "0.0.93",
"version": "0.0.94",
"description": "Reolink Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -797,6 +797,7 @@ class ReolinkProvider extends RtspProvider {
placeholder: '192.168.2.222',
},
{
subgroup: 'Advanced',
key: 'rtspChannel',
title: 'Channel Number Override',
description: "Optional: The channel number to use for snapshots and video. E.g., 0, 1, 2, etc.",
@@ -804,12 +805,14 @@ class ReolinkProvider extends RtspProvider {
type: 'number',
},
{
subgroup: 'Advanced',
key: 'httpPort',
title: 'HTTP Port',
description: 'Optional: Override the HTTP Port from the default value of 80.',
placeholder: '80',
},
{
subgroup: 'Advanced',
key: 'skipValidate',
title: 'Skip Validation',
description: 'Add the device without verifying the credentials and network settings.',

4
sdk/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.3.60",
"version": "0.3.61",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/sdk",
"version": "0.3.60",
"version": "0.3.61",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.24.7",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/sdk",
"version": "0.3.60",
"version": "0.3.61",
"description": "",
"main": "dist/src/index.js",
"exports": {

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/types",
"version": "0.3.56",
"version": "0.3.57",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/types",
"version": "0.3.56",
"version": "0.3.57",
"license": "ISC",
"devDependencies": {
"@types/node": "^22.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/types",
"version": "0.3.56",
"version": "0.3.57",
"description": "",
"main": "dist/index.js",
"author": "",

View File

@@ -648,7 +648,7 @@ class NotifierOptions(TypedDict):
dir: NotificationDirection
image: str
lang: str
recordedEvent: RecordedEvent
recordedEvent: Union[RecordedEvent, Any]
renotify: bool
requireInteraction: bool
silent: bool

View File

@@ -234,7 +234,7 @@ export interface NotifierOptions {
tag?: string;
timestamp?: number;
vibrate?: VibratePattern;
recordedEvent?: RecordedEvent;
recordedEvent?: RecordedEvent & { id: string };
// removed from typescript dom?
actions?: NotificationAction[];

View File

@@ -1,25 +1,25 @@
{
"name": "@scrypted/server",
"version": "0.115.45",
"version": "0.117.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.115.45",
"version": "0.117.3",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@scrypted/ffmpeg-static": "^6.1.0-build1",
"@scrypted/node-pty": "^1.0.18",
"@scrypted/types": "^0.3.56",
"adm-zip": "^0.5.15",
"@scrypted/types": "^0.3.57",
"adm-zip": "^0.5.16",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"engine.io": "^6.6.0",
"express": "^4.19.2",
"follow-redirects": "^1.15.6",
"follow-redirects": "^1.15.9",
"http-auth": "^4.2.0",
"level": "^8.0.1",
"lodash": "^4.17.21",
@@ -46,7 +46,7 @@
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
"@types/lodash": "^4.17.7",
"@types/node": "^22.5.0",
"@types/node": "^22.5.4",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.11",
"@types/semver": "^7.5.8",
@@ -557,9 +557,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.3.56",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.56.tgz",
"integrity": "sha512-SYP73isVSgVnFFokLKi4E5i3qXubSDMUjwUA4f4ZmLs0e2wSCP48NupGhKPavVay/k1E5PXjv8svTkMnAcNQOw=="
"version": "0.3.57",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.57.tgz",
"integrity": "sha512-XHE8RL2r8m5NGp+bsoUbeZ1zTHkGh5exXnrQFj2grUivZ9RY/D5wWxWieO+KHLSLstL7H71gMAuMxw6QQpnDXg=="
},
"node_modules/@types/adm-zip": {
"version": "0.5.5",
@@ -666,9 +666,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "22.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
"integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
"version": "22.5.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dependencies": {
"undici-types": "~6.19.2"
}
@@ -786,9 +786,9 @@
}
},
"node_modules/adm-zip": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.15.tgz",
"integrity": "sha512-jYPWSeOA8EFoZnucrKCNihqBjoEGQSU4HKgHYQgKNEQ0pQF9a/DYuo/+fAxY76k4qe75LUlLWpAM1QWcBMTOKw==",
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"engines": {
"node": ">=12.0"
}
@@ -1619,9 +1619,9 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",

View File

@@ -1,18 +1,18 @@
{
"name": "@scrypted/server",
"version": "0.115.45",
"version": "0.117.4",
"description": "",
"dependencies": {
"@scrypted/ffmpeg-static": "^6.1.0-build1",
"@scrypted/node-pty": "^1.0.18",
"@scrypted/types": "^0.3.56",
"adm-zip": "^0.5.15",
"@scrypted/types": "^0.3.57",
"adm-zip": "^0.5.16",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"engine.io": "^6.6.0",
"express": "^4.19.2",
"follow-redirects": "^1.15.6",
"follow-redirects": "^1.15.9",
"http-auth": "^4.2.0",
"level": "^8.0.1",
"lodash": "^4.17.21",
@@ -36,7 +36,7 @@
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
"@types/lodash": "^4.17.7",
"@types/node": "^22.5.0",
"@types/node": "^22.5.4",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.11",
"@types/semver": "^7.5.8",

View File

@@ -26,12 +26,15 @@ import rpc_reader
import scrypted_python.scrypted_sdk.types
from plugin_pip import install_with_pip, need_requirements, remove_pip_dirs
from scrypted_python.scrypted_sdk import PluginFork, ScryptedStatic
from scrypted_python.scrypted_sdk.types import (Device, DeviceManifest,
EventDetails,
ScryptedInterface,
ScryptedInterfaceMethods,
ScryptedInterfaceProperty,
Storage)
from scrypted_python.scrypted_sdk.types import (
Device,
DeviceManifest,
EventDetails,
ScryptedInterface,
ScryptedInterfaceMethods,
ScryptedInterfaceProperty,
Storage,
)
SCRYPTED_REQUIREMENTS = """
ptpython
@@ -509,19 +512,28 @@ class PluginRemote:
)
return base64.b64encode(m.digest()).decode("utf-8")
def onProxySerialization(value: Any, sourceKey: str = None):
def isClusterAddress(address: str):
return not address or address == SCRYPTED_CLUSTER_ADDRESS
def onProxySerialization(peer: rpc.RpcPeer, value: Any, sourceKey: str = None):
properties: dict = rpc.RpcPeer.prepareProxyProperties(value) or {}
clusterEntry = properties.get("__cluster", None)
proxyId: str = (
clusterEntry and clusterEntry.get("proxyId", None)
) or rpc.RpcPeer.generateId()
proxyId: str
existing = peer.localProxied.get(value, None)
if existing:
proxyId = existing["id"]
else:
proxyId = (
clusterEntry and clusterEntry.get("proxyId", None)
) or rpc.RpcPeer.generateId()
if (
clusterEntry
and clusterPort == clusterEntry["port"]
and sourceKey != clusterEntry.get("sourceKey", None)
):
clusterEntry = None
if clusterEntry:
if (
isClusterAddress(clusterEntry.get("address", None))
and clusterPort == clusterEntry["port"]
and sourceKey != clusterEntry.get("sourceKey", None)
):
clusterEntry = None
if not clusterEntry:
clusterEntry: ClusterObject = {
@@ -536,7 +548,9 @@ class PluginRemote:
return proxyId, properties
self.peer.onProxySerialization = onProxySerialization
self.peer.onProxySerialization = lambda value: onProxySerialization(
self.peer, value, None
)
async def resolveObject(id: str, sourceKey: str):
sourcePeer: rpc.RpcPeer = (
@@ -564,7 +578,7 @@ class PluginRemote:
self.loop, rpcTransport
)
peer.onProxySerialization = lambda value: onProxySerialization(
value, clusterPeerPort
peer, value, clusterPeerKey
)
future: asyncio.Future[rpc.RpcPeer] = asyncio.Future()
future.set_result(peer)
@@ -593,7 +607,7 @@ class PluginRemote:
clusterPort = clusterRpcServer.sockets[0].getsockname()[1]
def ensureClusterPeer(address: str, port: int):
if not address or address == SCRYPTED_CLUSTER_ADDRESS:
if isClusterAddress(address):
address = "127.0.0.1"
clusterPeerKey = getClusterPeerKey(address, port)
clusterPeerPromise = clusterPeers.get(clusterPeerKey)
@@ -601,21 +615,26 @@ class PluginRemote:
return clusterPeerPromise
async def connectClusterPeer():
reader, writer = await asyncio.open_connection(address, port)
sourceAddress, sourcePort = writer.get_extra_info("sockname")
if (
sourceAddress != SCRYPTED_CLUSTER_ADDRESS
and sourceAddress != "127.0.0.1"
):
print("source address mismatch", sourceAddress)
sourceKey = getClusterPeerKey(sourceAddress, sourcePort)
rpcTransport = rpc_reader.RpcStreamTransport(reader, writer)
clusterPeer, peerReadLoop = await rpc_reader.prepare_peer_readloop(
self.loop, rpcTransport
)
clusterPeer.onProxySerialization = lambda value: onProxySerialization(
value, sourceKey
)
try:
reader, writer = await asyncio.open_connection(address, port)
sourceAddress, sourcePort = writer.get_extra_info("sockname")
if (
sourceAddress != SCRYPTED_CLUSTER_ADDRESS
and sourceAddress != "127.0.0.1"
):
print("source address mismatch", sourceAddress)
rpcTransport = rpc_reader.RpcStreamTransport(reader, writer)
clusterPeer, peerReadLoop = await rpc_reader.prepare_peer_readloop(
self.loop, rpcTransport
)
clusterPeer.onProxySerialization = (
lambda value: onProxySerialization(
clusterPeer, value, clusterPeerKey
)
)
except:
clusterPeers.pop(clusterPeerKey)
raise
async def run_loop():
try:

View File

@@ -268,12 +268,21 @@ class RpcPeer:
proxiedEntry = self.localProxied.get(value, None)
if proxiedEntry:
if self.onProxySerialization:
proxyId, __remote_proxy_props = self.onProxySerialization(value)
else:
__remote_proxy_props = RpcPeer.prepareProxyProperties(value)
proxyId = proxiedEntry['id']
if proxyId != proxiedEntry['id']:
raise Exception('onProxySerialization proxy id mismatch')
proxiedEntry['finalizerId'] = RpcPeer.generateId()
ret = {
'__remote_proxy_id': proxiedEntry['id'],
'__remote_proxy_id': proxyId,
'__remote_proxy_finalizer_id': proxiedEntry['finalizerId'],
'__remote_constructor_name': __remote_constructor_name,
'__remote_proxy_props': RpcPeer.prepareProxyProperties(value),
'__remote_proxy_props': __remote_proxy_props,
'__remote_proxy_oneway_methods': getattr(value, '__proxy_oneway_methods', None),
}
return ret

View File

@@ -110,19 +110,25 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
const SCRYPTED_CLUSTER_ADDRESS = process.env.SCRYPTED_CLUSTER_ADDRESS;
const onProxySerialization = (value: any, sourceKey?: string) => {
function isClusterAddress(address: string) {
return !address || address === SCRYPTED_CLUSTER_ADDRESS;
}
const onProxySerialization = (peer: RpcPeer, value: any, sourceKey: string) => {
const properties = RpcPeer.prepareProxyProperties(value) || {};
let clusterEntry: ClusterObject = properties.__cluster;
// ensure globally stable proxyIds.
// worker threads will embed their pid and tid in the proxy id for cross worker fast path.
const proxyId = clusterEntry?.proxyId || (worker_threads.isMainThread ? RpcPeer.generateId() : `n-${process.pid}-${worker_threads.threadId}-${RpcPeer.generateId()}`);
const proxyId = peer.localProxied.get(value)?.id || clusterEntry?.proxyId || `n-${process.pid}-${worker_threads.threadId}-${RpcPeer.generateId()}`;
// if the cluster entry already exists, check if it belongs to this node.
// if it belongs to this node, the entry must also be for this peer.
// relying on the liveness/gc of a different peer may cause race conditions.
if (clusterEntry && clusterPort === clusterEntry.port && sourceKey !== clusterEntry.sourceKey)
clusterEntry = undefined;
if (clusterEntry) {
if (isClusterAddress(clusterEntry?.address) && clusterPort === clusterEntry.port && sourceKey !== clusterEntry.sourceKey)
clusterEntry = undefined;
}
if (!clusterEntry) {
clusterEntry = {
@@ -142,13 +148,21 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
properties,
};
}
peer.onProxySerialization = onProxySerialization;
peer.onProxySerialization = value => onProxySerialization(peer, value, undefined);
const resolveObject = async (id: string, sourceKey: string) => {
const sourcePeer = sourceKey
? await clusterPeers.get(sourceKey)
: peer;
return sourcePeer?.localProxyMap.get(id);
if (!sourcePeer)
console.error('source peer not found', sourceKey);
const ret = sourcePeer?.localProxyMap.get(id);
if (!ret) {
console.error('source key not found', sourceKey, id);
return;
}
return ret;
}
// all cluster clients, incoming and outgoing, connect with random ports which can be used as peer ids
@@ -161,13 +175,13 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
}
const clusterRpcServer = net.createServer(client => {
const clusterPeer = createDuplexRpcPeer(peer.selfName, 'cluster-client', client, client);
const clusterPeerAddress = client.remoteAddress;
const clusterPeerPort = client.remotePort;
const clusterPeerKey = getClusterPeerKey(clusterPeerAddress, clusterPeerPort);
const clusterPeer = createDuplexRpcPeer(peer.selfName, clusterPeerKey, client, client);
// the listening peer sourceKey (client address/port) is used by the OTHER peer (the client)
// to determine if it is already connected to THIS peer (the server).
clusterPeer.onProxySerialization = (value) => onProxySerialization(value, clusterPeerKey);
clusterPeer.onProxySerialization = (value) => onProxySerialization(clusterPeer, value, clusterPeerKey);
clusterPeers.set(clusterPeerKey, Promise.resolve(clusterPeer));
startPluginRemoteOptions?.onClusterPeer?.(clusterPeer);
const connectRPCObject: ConnectRPCObject = async (o) => {
@@ -189,7 +203,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
const clusterPort = await listenZero(clusterRpcServer, listenAddress);
const ensureClusterPeer = (address: string, connectPort: number) => {
if (!address || address === SCRYPTED_CLUSTER_ADDRESS)
if (isClusterAddress(address))
address = '127.0.0.1';
const clusterPeerKey = getClusterPeerKey(address, connectPort);
@@ -203,16 +217,12 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
try {
await once(socket, 'connect');
// this connecting peer sourceKey (server address/port) is used by the OTHER peer (the server)
// to determine if it is already connected to THIS peer (the client).
const { address: sourceAddress, port: sourcePort } = (socket.address() as net.AddressInfo);
const { address: sourceAddress } = (socket.address() as net.AddressInfo);
if (sourceAddress !== SCRYPTED_CLUSTER_ADDRESS && sourceAddress !== '127.0.0.1')
console.warn("source address mismatch", sourceAddress);
const sourcePeerKey = getClusterPeerKey(sourceAddress, sourcePort);
const clusterPeer = createDuplexRpcPeer(peer.selfName, 'cluster-server', socket, socket);
clusterPeer.onProxySerialization = (value) => onProxySerialization(value, sourcePeerKey);
const clusterPeer = createDuplexRpcPeer(peer.selfName, clusterPeerKey, socket, socket);
clusterPeer.onProxySerialization = (value) => onProxySerialization(clusterPeer, value, clusterPeerKey);
return clusterPeer;
}
catch (e) {
@@ -239,30 +249,6 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
const tidChannels = new Map<number, Deferred<worker_threads.MessagePort>>();
const tidPeers = new Map<number, Promise<RpcPeer>>();
function finishTidPeerConnection(tid: number, port: worker_threads.MessagePort) {
const threadPeer = NodeThreadWorker.createRpcPeer(peer.selfName, 'thread-server', port);
// this connecting peer sourceKey (thread id) is used by the OTHER peer (the server)
// to determine if it is already connected to THIS peer (the client).
const threadPeerKey = `thread:${worker_threads.threadId}-${tid}`;
threadPeer.onProxySerialization = value => onProxySerialization(value, threadPeerKey);
const connectRPCObject: ConnectRPCObject = async (o) => {
const sha256 = computeClusterObjectHash(o, clusterSecret);
if (sha256 !== o.sha256)
throw new Error('secret incorrect');
return resolveObject(o.proxyId, o.sourceKey);
}
threadPeer.params['connectRPCObject'] = connectRPCObject;
function cleanup(message: string) {
tidChannels.delete(tid);
tidPeers.delete(tid);
threadPeer.kill(message);
}
port.on('close', () => cleanup('connection closed.'));
port.on('messageerror', () => cleanup('message error.'));
return threadPeer;
}
function connectTidPeer(tid: number) {
let peerPromise = tidPeers.get(tid);
if (peerPromise)
@@ -281,17 +267,33 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
}
}
function cleanup() {
clusterPeers.delete(threadPeerKey);
const threadPeerKey = `thread:${tid}`;
function peerCleanup() {
clusterPeers.delete(threadPeerKey);
}
peerPromise = tidDeferred.promise.then(port => {
port.on('close', () => cleanup());
port.on('messageerror', () => cleanup());
return finishTidPeerConnection(tid, port);
const threadPeer = NodeThreadWorker.createRpcPeer(peer.selfName, threadPeerKey, port);
threadPeer.onProxySerialization = value => onProxySerialization(threadPeer, value, threadPeerKey);
const connectRPCObject: ConnectRPCObject = async (o) => {
const sha256 = computeClusterObjectHash(o, clusterSecret);
if (sha256 !== o.sha256)
throw new Error('secret incorrect');
return resolveObject(o.proxyId, o.sourceKey);
}
threadPeer.params['connectRPCObject'] = connectRPCObject;
function cleanup(message: string) {
peerCleanup();
tidChannels.delete(tid);
tidPeers.delete(tid);
threadPeer.kill(message);
}
port.on('close', () => cleanup('connection closed.'));
port.on('messageerror', () => cleanup('message error.'));
return threadPeer;
});
peerPromise.catch(() => cleanup());
const threadPeerKey = `thread:${worker_threads.threadId}-${tid}`;
peerPromise.catch(() => peerCleanup());
clusterPeers.set(threadPeerKey, peerPromise);
tidPeers.set(tid, peerPromise);
@@ -414,7 +416,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
return newValue;
}
catch (e) {
console.error('failure rpc', e);
console.error('failure rpc', clusterObject, e);
return value;
}
}

View File

@@ -442,7 +442,7 @@ export class RpcPeer {
result.reject(error);
}
for (const y of this.yieldedAsyncIterators) {
y.throw(error).catch(() => {});
y.throw(error).catch(() => { });
}
this.yieldedAsyncIterators.clear();
this.pendingResults = Object.freeze({});
@@ -594,13 +594,25 @@ export class RpcPeer {
let proxiedEntry = this.localProxied.get(value);
if (proxiedEntry) {
const {
proxyId: __remote_proxy_id,
properties: __remote_proxy_props,
} = this.onProxySerialization?.(value)
|| {
proxyId: proxiedEntry.id,
properties: RpcPeer.prepareProxyProperties(value),
};
if (__remote_proxy_id !== proxiedEntry.id)
throw new Error('onProxySerialization proxy id mismatch');
const __remote_proxy_finalizer_id = RpcPeer.generateId();
proxiedEntry.finalizerId = __remote_proxy_finalizer_id;
const ret: RpcRemoteProxyValue = {
__remote_proxy_id: proxiedEntry.id,
__remote_proxy_id,
__remote_proxy_finalizer_id,
__remote_constructor_name,
__remote_proxy_props: RpcPeer.prepareProxyProperties(value),
__remote_proxy_props,
__remote_proxy_oneway_methods: value?.[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS],
}
return ret;
@@ -619,12 +631,11 @@ export class RpcPeer {
const {
proxyId: __remote_proxy_id,
properties: __remote_proxy_props,
} = this.onProxySerialization
? this.onProxySerialization(value)
: {
proxyId: RpcPeer.generateId(),
properties: RpcPeer.prepareProxyProperties(value),
};
} = this.onProxySerialization?.(value)
|| {
proxyId: RpcPeer.generateId(),
properties: RpcPeer.prepareProxyProperties(value),
};
proxiedEntry = {
id: __remote_proxy_id,
@@ -742,7 +753,7 @@ export class RpcPeer {
}
else {
if (Object.isFrozen(this.pendingResults)) {
(target as AsyncGenerator).throw(new RPCResultError(this, 'RpcPeer has been killed (yield)')).catch(() => {});
(target as AsyncGenerator).throw(new RPCResultError(this, 'RpcPeer has been killed (yield)')).catch(() => { });
}
else {
this.yieldedAsyncIterators.add(target);

View File

@@ -740,7 +740,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
return pluginHost;
}
findPluginDevice?(pluginId: string, nativeId?: ScryptedNativeId): PluginDevice {
findPluginDevice(pluginId: string, nativeId?: ScryptedNativeId): PluginDevice {
// JSON stringify over rpc turns undefined into null.
if (nativeId === null)
nativeId = undefined;

View File

@@ -5,14 +5,14 @@ import { startPluginRemote } from "./plugin/plugin-remote-worker";
import { SidebandSocketSerializer } from "./plugin/socket-serializer";
import { RpcMessage } from "./rpc";
import { NodeThreadWorker } from './plugin/runtime/node-thread-worker';
import { isNodePluginThreadProcess } from './plugin/runtime/node-fork-worker';
import { isNodePluginForkProcess, isNodePluginThreadProcess } from './plugin/runtime/node-fork-worker';
function start(mainFilename: string) {
const pluginId = process.argv[3];
module.paths.push(getPluginNodePath(pluginId));
if (isNodePluginThreadProcess()) {
console.log('starting thread', pluginId);
console.log('starting thread', pluginId, process.pid, worker_threads.threadId);
const { port } = worker_threads.workerData as { port: worker_threads.MessagePort };
const peer = startPluginRemote(mainFilename, pluginId, (message, reject, serializationContext) => NodeThreadWorker.send(message, port, reject, serializationContext));
NodeThreadWorker.setupRpcPeer(peer, port);
@@ -26,7 +26,10 @@ function start(mainFilename: string) {
});
}
else {
console.log('starting plugin', pluginId);
if (isNodePluginForkProcess())
console.log('starting fork', pluginId, process.pid);
else
console.log('starting plugin', pluginId, process.pid);
const peer = startPluginRemote(mainFilename, process.argv[3], (message, reject, serializationContext) => process.send(message, serializationContext?.sendHandle, {
// what happened to this argument?
// swallowErrors: !reject,