core: use new builtin docker image updater

This commit is contained in:
Koushik Dutta
2025-11-16 20:48:34 -08:00
parent b43fdf83e2
commit b69dd024e5
2 changed files with 140 additions and 6 deletions

View File

@@ -12,7 +12,7 @@ import { AutomationCore, AutomationCoreNativeId } from './automations-core';
import { ClusterCore, ClusterCoreNativeId } from './cluster';
import { LauncherMixin } from './launcher-mixin';
import { MediaCore } from './media-core';
import { checkLegacyLxc, checkLxc } from './platform/lxc';
import { checkLegacyLxc, checkLxc, checkLxcVersionUpdateNeeded } from './platform/lxc';
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
import { TerminalService, TerminalServiceNativeId, newTerminalService } from './terminal-service';
@@ -215,9 +215,14 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
);
})();
// check on workers once an hour.
// check on workers immediately and once an hour.
this.updateWorkers();
setInterval(() => this.updateWorkers(), 1000 * 60 * 60);
setInterval(() => this.updateWorkers(), 60 * 1000 * 60);
// check on worker images once an hour.
// checking immediately is problematic as a failed update may cause a restart loop on startup.
// images are also pruned 1 minute after startup, so avoid that.
setInterval(() => this.updateWorkerImages(), 60 * 1000 * 60);
}
async updateWorkers() {
@@ -242,6 +247,38 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
}
}
async updateWorkerImages() {
const workers = await sdk.clusterManager?.getClusterWorkers();
if (!workers)
return;
for (const [id, worker] of Object.entries(workers)) {
const forked = sdk.fork<ReturnType<typeof fork>>({
clusterWorkerId: id,
runtime: 'node',
});
(async () => {
try {
const result = await forked.result;
if (!await result.checkLxcVersionUpdateNeeded()) {
return;
}
// restart the worker to pick up the new image.
const clusterFork = await sdk.systemManager.getComponent('cluster-fork');
const serviceControl = await clusterFork.getServiceControl(worker.id);
await serviceControl.restart().catch(() => { });
}
catch (e) {
}
finally {
await sleep(1000);
forked.worker.terminate();
}
})();
}
}
async getSettings(): Promise<Setting[]> {
try {
const service = await sdk.systemManager.getComponent('addresses');
@@ -361,6 +398,7 @@ export async function fork() {
tsCompile,
newScript,
newTerminalService,
checkLxcVersionUpdateNeeded,
checkLxc: async () => {
try {
// console.warn('Checking for LXC installation...');

View File

@@ -1,7 +1,9 @@
import fs, { writeFileSync } from 'fs';
import sdk from '@scrypted/sdk';
import yaml from 'yaml';
import { Deferred } from '@scrypted/common/src/deferred';
import { readFileAsString } from '@scrypted/common/src/eval/scrypted-eval';
import sdk from '@scrypted/sdk';
import fs, { writeFileSync } from 'fs';
import http from 'http';
import yaml from 'yaml';
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC = 'lxc';
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER = 'lxc-docker';
@@ -24,6 +26,100 @@ export async function checkLxc() {
await checkLxcScript();
}
async function dockerRequest(options: http.RequestOptions, body?: string) {
const deferred = new Deferred<string>();
const req = http.request({
socketPath: '/var/run/docker.sock',
method: options.method,
path: options.path,
headers: {
'Host': 'localhost',
...options.headers
}
});
req.on('response', (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
deferred.resolve(data);
});
});
req.on('error', (err) => {
deferred.reject(err);
});
if (body) {
req.write(body);
}
req.end();
return deferred.promise;
}
async function dockerPullScryptedTag(tag: string) {
return dockerRequest({
method: 'POST',
path: `/v1.41/images/create?fromImage=ghcr.io%2Fkoush%2Fscrypted&tag=${tag}`,
});
}
async function dockerImageLsScryptedTag(tag: string) {
// List all images and find the specific one
const data = await dockerRequest({
method: 'GET',
path: '/v1.41/images/json'
});
const images = JSON.parse(data);
// Filter for your specific image
const targetImage = images.find(image => {
return image.RepoTags && image.RepoTags.some(t =>
t === `ghcr.io/koush/scrypted:${tag}`
);
});
if (!targetImage) {
throw new Error('Image not found');
}
return targetImage.Id;
}
async function dockerGetScryptedContainerImageId() {
// List running containers filtered by name
const data = await dockerRequest({
method: 'GET',
path: '/v1.41/containers/json?filters={"name":["scrypted"],"status":["running"]}'
});
const containers = JSON.parse(data);
if (!containers.length)
throw new Error('No running container named "scrypted" found');
const container = containers[0];
return container.ImageID;
}
export async function checkLxcVersionUpdateNeeded() {
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER)
return;
const dockerCompose = yaml.parseDocument(readFileAsString('/root/.scrypted/docker-compose.yml'));
// @ts-ignore
const image: string = dockerCompose.contents.get('services').get('scrypted').get('image');
const label = image.split(':')[1] || 'latest';
await dockerPullScryptedTag(label);
const imageId = await dockerImageLsScryptedTag(label);
const containerImageId = await dockerGetScryptedContainerImageId();
console.warn('LXC Scrypted latest image ID:', imageId);
console.warn('LXC Scrypted running image ID:', containerImageId);
return containerImageId !== imageId;
}
async function checkLxcCompose() {
// the lxc-docker used watchtower for automatic updates but watchtower started crashing in the lxc environment
// after a docker update.