mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
ed96e94806
- Updated session control mechanisms to ensure proper handling of remote input and clipboard operations. - Introduced session authorization checks to validate operator permissions before starting desktop sessions. - Improved input injection logic to prevent unauthorized access during active sessions. - Added new capabilities for managing session flags and controls, ensuring a more robust and secure desktop experience. - Enhanced error handling and logging for better traceability of session-related actions.
568 lines
21 KiB
JavaScript
568 lines
21 KiB
JavaScript
/**
|
|
* BetterDesk Console — Agent Client build worker (Tauri + Go sidecar)
|
|
*
|
|
* Builds branded betterdesk-agent-client installers when generator bundles
|
|
* have product_type=agent-client.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const fsp = fs.promises;
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const { spawn } = require('child_process');
|
|
|
|
const db = require('./database');
|
|
const bundleService = require('./agentBundleService');
|
|
const config = require('../config/config');
|
|
const {
|
|
PRODUCT_TYPES,
|
|
normalizeProductType,
|
|
isQueuedBuildStatus,
|
|
} = require('../lib/generatorBuildTypes');
|
|
|
|
try {
|
|
const envFile = process.env.BETTERDESK_BUILD_ENV_FILE || '/etc/betterdesk/build.env';
|
|
if (fs.existsSync(envFile)) {
|
|
const txt = fs.readFileSync(envFile, 'utf8');
|
|
for (const line of txt.split(/\r?\n/)) {
|
|
const m = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
|
|
if (!m) continue;
|
|
const [, key, val] = m;
|
|
if (process.env[key] === undefined) process.env[key] = val;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('[agentClientBuildWorker] could not load build env file:', e.message);
|
|
}
|
|
|
|
const BUILD_USER = process.env.BUILD_USER || 'betterdesk';
|
|
const BUILD_CACHE_DIR = process.env.CARGO_TARGET_DIR
|
|
|| process.env.BUILD_CACHE_DIR
|
|
|| path.join(config.dataDir || '/opt/BetterDeskConsole/data', 'build-cache');
|
|
const WORK_ROOT = path.join(BUILD_CACHE_DIR, 'agent-client-work');
|
|
const AGENT_LIB_CACHE = path.join(BUILD_CACHE_DIR, 'agent-client-lib');
|
|
const ARTIFACT_ROOT = process.env.AGENT_CLIENT_ARTIFACT_DIR
|
|
|| path.join(config.dataDir || '/opt/BetterDeskConsole/data', 'agent-client-builds');
|
|
const POLL_INTERVAL_MS = parseInt(process.env.AGENT_CLIENT_BUILD_POLL_MS || '8000', 10);
|
|
const BUILD_TIMEOUT_MS = parseInt(process.env.AGENT_CLIENT_BUILD_TIMEOUT_MS || (45 * 60 * 1000), 10);
|
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
|
function _resolveSourceRoot() {
|
|
if (process.env.AGENT_CLIENT_SOURCE_DIR) return process.env.AGENT_CLIENT_SOURCE_DIR;
|
|
const candidates = [
|
|
path.join(REPO_ROOT, 'betterdesk-agent-client'),
|
|
'/opt/BetterDeskConsole/agent-source/betterdesk-agent-client',
|
|
];
|
|
for (const c of candidates) {
|
|
if (fs.existsSync(path.join(c, 'src-tauri', 'Cargo.toml'))) return c;
|
|
}
|
|
return candidates[0];
|
|
}
|
|
|
|
function _resolveAgentLibRoot() {
|
|
if (process.env.AGENT_LIB_DIR) return process.env.AGENT_LIB_DIR;
|
|
const candidates = [
|
|
path.join(REPO_ROOT, 'betterdesk-agent'),
|
|
'/opt/BetterDeskConsole/agent-source/betterdesk-agent',
|
|
];
|
|
for (const c of candidates) {
|
|
if (fs.existsSync(path.join(c, 'go.mod'))) return c;
|
|
}
|
|
return candidates[0];
|
|
}
|
|
|
|
const SOURCE_ROOT = _resolveSourceRoot();
|
|
const AGENT_LIB_ROOT = _resolveAgentLibRoot();
|
|
const CARGO_HOME = process.env.CARGO_HOME || `/home/${BUILD_USER}/.cargo`;
|
|
|
|
function _resolveBin(candidates) {
|
|
for (const c of candidates) {
|
|
if (fs.existsSync(c)) return c;
|
|
}
|
|
return candidates[0];
|
|
}
|
|
|
|
const CARGO_BIN = process.env.CARGO_BIN || _resolveBin([
|
|
`${CARGO_HOME}/bin/cargo`,
|
|
'/usr/local/bin/cargo',
|
|
'/usr/bin/cargo',
|
|
]);
|
|
const NPM_BIN = process.env.NPM_BIN || _resolveBin([
|
|
'/usr/bin/npm',
|
|
'/usr/local/bin/npm',
|
|
]);
|
|
|
|
const BASE_BUILD_ENV = {
|
|
PATH: `${CARGO_HOME}/bin:/usr/local/bin:/usr/bin:/bin`,
|
|
HOME: `/home/${BUILD_USER}`,
|
|
CARGO_HOME,
|
|
RUSTUP_HOME: process.env.RUSTUP_HOME || `/home/${BUILD_USER}/.rustup`,
|
|
CARGO_TARGET_DIR: BUILD_CACHE_DIR,
|
|
DEBIAN_FRONTEND: 'noninteractive',
|
|
LANG: 'C.UTF-8',
|
|
APPIMAGE_EXTRACT_AND_RUN: '1',
|
|
NO_STRIP: '1',
|
|
};
|
|
|
|
const BUILD_PROFILES = {
|
|
'windows/x64/portable': {
|
|
os: 'windows', target: 'x86_64-pc-windows-msvc', bundles: ['nsis'], artifact: 'exe', runner: 'cargo-xwin',
|
|
},
|
|
'windows/x64/installed': {
|
|
os: 'windows', target: 'x86_64-pc-windows-msvc', bundles: ['msi'], artifact: 'msi', runner: 'cargo-xwin',
|
|
},
|
|
'linux/x64/portable': { os: 'linux', target: null, bundles: [], artifact: 'tgz' },
|
|
'linux/x64/appimage': { os: 'linux', target: null, bundles: ['appimage'], artifact: 'appimage' },
|
|
'linux/x64/installed': { os: 'linux', target: null, bundles: ['deb'], artifact: 'deb' },
|
|
'linux/x64/rpm': { os: 'linux', target: null, bundles: ['rpm'], artifact: 'rpm' },
|
|
};
|
|
|
|
const SIDECAR_NAMES = {
|
|
'linux/x64': 'betterdesk-agent-x86_64-unknown-linux-gnu',
|
|
'windows/x64': 'betterdesk-agent-x86_64-pc-windows-msvc.exe',
|
|
};
|
|
|
|
function _isAgentClientBundle(bundle) {
|
|
return normalizeProductType(bundle?.product_type) === PRODUCT_TYPES.AGENT_CLIENT;
|
|
}
|
|
|
|
let _pollTimer = null;
|
|
let _running = false;
|
|
let _activeBuilds = 0;
|
|
|
|
function _run(cmd, args, opts = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(cmd, args, {
|
|
cwd: opts.cwd,
|
|
env: { ...BASE_BUILD_ENV, ...(opts.env || {}) },
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let out = '';
|
|
child.stdout.on('data', (d) => { out += d.toString(); });
|
|
child.stderr.on('data', (d) => { out += d.toString(); });
|
|
const timer = setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
reject(new Error(`timeout after ${BUILD_TIMEOUT_MS}ms`));
|
|
}, BUILD_TIMEOUT_MS);
|
|
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
child.on('close', (code) => {
|
|
clearTimeout(timer);
|
|
if (code === 0) resolve(out);
|
|
else reject(new Error(`${cmd} exited ${code}: ${out.slice(-4000)}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function _copyDir(src, dest) {
|
|
const SKIP = new Set(['node_modules', 'target', 'dist', '.git', 'data']);
|
|
await fsp.mkdir(dest, { recursive: true });
|
|
const entries = await fsp.readdir(src, { withFileTypes: true });
|
|
for (const ent of entries) {
|
|
if (SKIP.has(ent.name)) continue;
|
|
const s = path.join(src, ent.name);
|
|
const d = path.join(dest, ent.name);
|
|
if (ent.isDirectory()) await _copyDir(s, d);
|
|
else await fsp.copyFile(s, d);
|
|
}
|
|
}
|
|
|
|
function _agentClientBranding(branding) {
|
|
const b = { ...(branding || {}) };
|
|
delete b.enrollment_token;
|
|
delete b.has_enrollment_token;
|
|
delete b.enrollment_token_masked;
|
|
return {
|
|
product_name: b.product_name || b.company_name || 'BetterDesk Agent',
|
|
company_name: b.company_name || 'BetterDesk',
|
|
tagline: b.short_text || b.tagline || '',
|
|
short_text: b.short_text || b.tagline || '',
|
|
support_email: b.contact_email || b.support_email || '',
|
|
contact_email: b.contact_email || b.support_email || '',
|
|
support_phone: b.contact_phone || b.support_phone || '',
|
|
contact_phone: b.contact_phone || b.support_phone || '',
|
|
contact_url: b.contact_url || '',
|
|
primary_color: b.primary_color || '#2563eb',
|
|
accent_color: b.accent_color || '#0ea5e9',
|
|
logo_data_url: b.logo_data_url || '',
|
|
default_language: b.default_lang || b.default_language || 'en',
|
|
default_lang: b.default_lang || b.default_language || 'en',
|
|
allow_unattended: !!b.allow_unattended,
|
|
server_address: b.server_address || b.server?.address || '',
|
|
server_key: b.server_key || b.server?.public_key || '',
|
|
bundle_id: b.bundle_id || '',
|
|
server: b.server || undefined,
|
|
};
|
|
}
|
|
|
|
async function _ensureAgentLib() {
|
|
if (!fs.existsSync(path.join(AGENT_LIB_ROOT, 'go.mod'))) {
|
|
throw new Error(`betterdesk-agent source missing at ${AGENT_LIB_ROOT}`);
|
|
}
|
|
if (fs.existsSync(AGENT_LIB_CACHE)) {
|
|
await fsp.rm(AGENT_LIB_CACHE, { recursive: true, force: true });
|
|
}
|
|
await _copyDir(AGENT_LIB_ROOT, AGENT_LIB_CACHE);
|
|
return AGENT_LIB_CACHE;
|
|
}
|
|
|
|
async function _buildSidecar(profile, destPath) {
|
|
const agentBuildWorker = require('./agentBuildWorker');
|
|
const goBin = typeof agentBuildWorker.getGoBin === 'function'
|
|
? agentBuildWorker.getGoBin()
|
|
: null;
|
|
if (!goBin) throw new Error('Go toolchain not available for sidecar build');
|
|
|
|
const agentLib = await _ensureAgentLib();
|
|
const goos = profile.os === 'windows' ? 'windows' : 'linux';
|
|
const goarch = 'amd64';
|
|
await fsp.mkdir(path.dirname(destPath), { recursive: true });
|
|
await _run(goBin, [
|
|
'build', '-ldflags', '-s -w',
|
|
'-o', destPath,
|
|
'.',
|
|
], {
|
|
cwd: agentLib,
|
|
env: { CGO_ENABLED: '0', GOOS: goos, GOARCH: goarch },
|
|
});
|
|
if (profile.os === 'linux') {
|
|
await fsp.chmod(destPath, 0o755);
|
|
}
|
|
}
|
|
|
|
async function _materialiseWorkDir(hash, branding, profile) {
|
|
const workDir = path.join(WORK_ROOT, hash.slice(0, 16));
|
|
if (fs.existsSync(workDir)) {
|
|
await fsp.rm(workDir, { recursive: true, force: true });
|
|
}
|
|
await _copyDir(SOURCE_ROOT, workDir);
|
|
|
|
const sidecarKey = `${profile.os}/${profile.target ? 'x64' : 'x64'}`;
|
|
const sidecarName = SIDECAR_NAMES[sidecarKey]
|
|
|| (profile.os === 'windows'
|
|
? 'betterdesk-agent-x86_64-pc-windows-msvc.exe'
|
|
: 'betterdesk-agent-x86_64-unknown-linux-gnu');
|
|
const binariesDir = path.join(workDir, 'src-tauri', 'binaries');
|
|
await fsp.mkdir(binariesDir, { recursive: true });
|
|
const sidecarDest = path.join(binariesDir, sidecarName);
|
|
await _buildSidecar(profile, sidecarDest);
|
|
|
|
const agentLibSibling = path.join(path.dirname(workDir), 'betterdesk-agent');
|
|
if (fs.existsSync(agentLibSibling)) {
|
|
await fsp.rm(agentLibSibling, { recursive: true, force: true });
|
|
}
|
|
await _copyDir(AGENT_LIB_ROOT, agentLibSibling);
|
|
|
|
const resDir = path.join(workDir, 'src-tauri', 'resources');
|
|
await fsp.mkdir(resDir, { recursive: true });
|
|
await fsp.writeFile(
|
|
path.join(resDir, 'branding.json'),
|
|
JSON.stringify(_agentClientBranding(branding), null, 2),
|
|
'utf8'
|
|
);
|
|
return workDir;
|
|
}
|
|
|
|
async function _findArtifact(workDir, profile, key) {
|
|
const bundleBase = profile.target
|
|
? path.join(BUILD_CACHE_DIR, profile.target, 'release', 'bundle')
|
|
: path.join(BUILD_CACHE_DIR, 'release', 'bundle');
|
|
const releaseDir = profile.target
|
|
? path.join(BUILD_CACHE_DIR, profile.target, 'release')
|
|
: path.join(BUILD_CACHE_DIR, 'release');
|
|
|
|
if (profile.artifact === 'deb') {
|
|
const debDir = path.join(bundleBase, 'deb');
|
|
const files = fs.existsSync(debDir) ? await fsp.readdir(debDir) : [];
|
|
const deb = files.find((f) => f.endsWith('.deb'));
|
|
if (deb) return path.join(debDir, deb);
|
|
}
|
|
if (profile.artifact === 'rpm') {
|
|
const rpmDir = path.join(bundleBase, 'rpm');
|
|
const files = fs.existsSync(rpmDir) ? await fsp.readdir(rpmDir) : [];
|
|
const rpm = files.find((f) => f.endsWith('.rpm'));
|
|
if (rpm) return path.join(rpmDir, rpm);
|
|
}
|
|
if (profile.artifact === 'appimage') {
|
|
const aiDir = path.join(bundleBase, 'appimage');
|
|
const files = fs.existsSync(aiDir) ? await fsp.readdir(aiDir) : [];
|
|
const ai = files.find((f) => f.endsWith('.AppImage'));
|
|
if (ai) return path.join(aiDir, ai);
|
|
}
|
|
if (profile.artifact === 'msi') {
|
|
const msiDir = path.join(bundleBase, 'msi');
|
|
const files = fs.existsSync(msiDir) ? await fsp.readdir(msiDir) : [];
|
|
const msi = files.find((f) => f.endsWith('.msi'));
|
|
if (msi) return path.join(msiDir, msi);
|
|
}
|
|
if (profile.artifact === 'exe') {
|
|
const nsisDir = path.join(bundleBase, 'nsis');
|
|
const files = fs.existsSync(nsisDir) ? await fsp.readdir(nsisDir) : [];
|
|
const exe = files.find((f) => f.endsWith('-setup.exe') || f.endsWith('.exe'));
|
|
if (exe) return path.join(nsisDir, exe);
|
|
for (const n of ['betterdesk-agent-client.exe', 'BetterDesk Agent.exe']) {
|
|
const p = path.join(releaseDir, n);
|
|
if (fs.existsSync(p)) return p;
|
|
}
|
|
}
|
|
if (profile.artifact === 'tgz') {
|
|
const names = ['betterdesk-agent-client', 'BetterDesk Agent'];
|
|
for (const n of names) {
|
|
const bin = path.join(releaseDir, n);
|
|
if (fs.existsSync(bin)) {
|
|
const stage = path.join(workDir, 'dist-portable');
|
|
await fsp.mkdir(stage, { recursive: true });
|
|
await fsp.copyFile(bin, path.join(stage, n));
|
|
await fsp.copyFile(
|
|
path.join(workDir, 'src-tauri', 'binaries', SIDECAR_NAMES['linux/x64']),
|
|
path.join(stage, SIDECAR_NAMES['linux/x64'])
|
|
);
|
|
const tarPath = path.join(ARTIFACT_ROOT, `${key.replace(/\//g, '-')}.tar.gz`);
|
|
await fsp.mkdir(path.dirname(tarPath), { recursive: true });
|
|
await _run('tar', ['-czf', tarPath, '-C', stage, '.']);
|
|
return tarPath;
|
|
}
|
|
}
|
|
}
|
|
throw new Error(`artifact not found for ${key}`);
|
|
}
|
|
|
|
async function _sha256(filePath) {
|
|
return new Promise((resolve, reject) => {
|
|
const h = crypto.createHash('sha256');
|
|
const s = fs.createReadStream(filePath);
|
|
s.on('error', reject);
|
|
s.on('data', (d) => h.update(d));
|
|
s.on('end', () => resolve(h.digest('hex')));
|
|
});
|
|
}
|
|
|
|
async function _findBundleForHash(hash) {
|
|
const all = await db.listAgentBundles({ includeRevoked: true });
|
|
return all.find((b) => b.branding_hash === hash) || null;
|
|
}
|
|
|
|
async function _listPendingAgentClientBuilds(limit) {
|
|
const bundles = await db.listAgentBundles();
|
|
const out = [];
|
|
for (const b of bundles) {
|
|
if (b.revoked || !_isAgentClientBundle(b)) continue;
|
|
const builds = await db.listAgentBundleBuildsForHash(b.branding_hash);
|
|
for (const r of builds) {
|
|
if (isQueuedBuildStatus(r.status)) out.push(r);
|
|
if (out.length >= limit) break;
|
|
}
|
|
if (out.length >= limit) break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
async function _hasAgentClientBuildInProgress() {
|
|
if (_activeBuilds > 0) return true;
|
|
const bundles = await db.listAgentBundles();
|
|
for (const b of bundles) {
|
|
if (!_isAgentClientBundle(b) || b.revoked) continue;
|
|
const builds = await db.listAgentBundleBuildsForHash(b.branding_hash);
|
|
if (builds.some((r) => r.status === 'building')) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function enqueueBuildsForHash(brandingHash, { force = false } = {}) {
|
|
const platforms = bundleService.PLATFORMS || [];
|
|
for (const p of platforms) {
|
|
const existing = await db.getAgentBundleBuild({
|
|
brandingHash, platform: p.platform, arch: p.arch, format: p.format,
|
|
});
|
|
if (!force && existing && (existing.status === 'ready' || existing.status === 'building')) {
|
|
continue;
|
|
}
|
|
await db.upsertAgentBundleBuild({
|
|
brandingHash,
|
|
platform: p.platform,
|
|
arch: p.arch,
|
|
format: p.format,
|
|
status: 'queued',
|
|
artifactPath: existing?.artifact_path || null,
|
|
artifactSize: existing?.artifact_size || 0,
|
|
artifactSha256: existing?.artifact_sha256 || null,
|
|
errorMessage: '',
|
|
});
|
|
}
|
|
}
|
|
|
|
async function requeueAllBundleBuilds() {
|
|
const bundles = await db.listAgentBundles({ includeRevoked: false });
|
|
const hashes = [...new Set(
|
|
bundles
|
|
.filter((bundle) => !bundle.revoked && _isAgentClientBundle(bundle))
|
|
.map((bundle) => bundle.branding_hash)
|
|
.filter(Boolean)
|
|
)];
|
|
for (const hash of hashes) {
|
|
await enqueueBuildsForHash(hash, { force: true });
|
|
}
|
|
return { bundles: hashes.length };
|
|
}
|
|
|
|
async function rebuildBundleById(bundleId) {
|
|
const row = await db.getAgentBundle(bundleId);
|
|
if (!row) return { success: false, error: 'not_found' };
|
|
if (!_isAgentClientBundle(row)) {
|
|
return { success: false, error: 'not_agent_client' };
|
|
}
|
|
if (!row.branding_hash) return { success: false, error: 'missing_hash' };
|
|
await enqueueBuildsForHash(row.branding_hash, { force: true });
|
|
return { success: true, platforms: (bundleService.PLATFORMS || []).length };
|
|
}
|
|
|
|
async function requeuePlatformBuild(brandingHash, platform, arch, format) {
|
|
if (!brandingHash || !platform || !arch || !format) {
|
|
return { success: false, error: 'missing_args' };
|
|
}
|
|
const allowed = (bundleService.PLATFORMS || []).some(
|
|
(p) => p.platform === platform && p.arch === arch && p.format === format
|
|
);
|
|
if (!allowed) return { success: false, error: 'unsupported_platform' };
|
|
const bundle = await _findBundleForHash(brandingHash);
|
|
if (!bundle || !_isAgentClientBundle(bundle)) {
|
|
return { success: false, error: 'not_agent_client' };
|
|
}
|
|
await db.upsertAgentBundleBuild({
|
|
brandingHash,
|
|
platform,
|
|
arch,
|
|
format,
|
|
status: 'queued',
|
|
artifactPath: null,
|
|
artifactSize: 0,
|
|
artifactSha256: null,
|
|
errorMessage: '',
|
|
});
|
|
return { success: true };
|
|
}
|
|
|
|
async function _runOne(buildRow) {
|
|
const key = `${buildRow.platform}/${buildRow.arch}/${buildRow.format}`;
|
|
const profile = BUILD_PROFILES[key];
|
|
if (!profile) throw new Error(`unsupported profile ${key}`);
|
|
|
|
const bundleRow = await _findBundleForHash(buildRow.branding_hash);
|
|
if (!bundleRow || !_isAgentClientBundle(bundleRow)) {
|
|
throw new Error('not an agent-client bundle');
|
|
}
|
|
|
|
const branding = JSON.parse(bundleRow.branding || '{}');
|
|
console.log(`[agentClientBuildWorker] build start ${key} hash=${buildRow.branding_hash.slice(0, 12)}`);
|
|
const workDir = await _materialiseWorkDir(buildRow.branding_hash, branding, profile);
|
|
await _run(NPM_BIN, ['ci', '--no-audit', '--no-fund'], { cwd: workDir });
|
|
|
|
const npmTauriArgs = ['run', 'tauri', 'build'];
|
|
if (profile.runner) npmTauriArgs.push('--runner', profile.runner);
|
|
if (profile.target || profile.bundles.length) {
|
|
npmTauriArgs.push('--');
|
|
if (profile.target) npmTauriArgs.push('--target', profile.target);
|
|
if (profile.bundles.length) npmTauriArgs.push('--bundles', profile.bundles.join(','));
|
|
}
|
|
await _run(NPM_BIN, npmTauriArgs, { cwd: workDir });
|
|
|
|
const built = await _findArtifact(workDir, profile, key);
|
|
const destDir = path.join(ARTIFACT_ROOT, buildRow.branding_hash.slice(0, 16));
|
|
await fsp.mkdir(destDir, { recursive: true });
|
|
const destName = path.basename(built);
|
|
const destPath = path.join(destDir, destName);
|
|
if (built !== destPath) await fsp.copyFile(built, destPath);
|
|
const stat = await fsp.stat(destPath);
|
|
const sha = await _sha256(destPath);
|
|
|
|
await db.upsertAgentBundleBuild({
|
|
brandingHash: buildRow.branding_hash,
|
|
platform: buildRow.platform,
|
|
arch: buildRow.arch,
|
|
format: buildRow.format,
|
|
status: 'ready',
|
|
artifactPath: destPath,
|
|
artifactSize: stat.size,
|
|
artifactSha256: sha,
|
|
errorMessage: '',
|
|
});
|
|
console.log(`[agentClientBuildWorker] build ready ${key} (${(stat.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
}
|
|
|
|
async function _tick() {
|
|
if (_running || _activeBuilds > 0) return;
|
|
if (await _hasAgentClientBuildInProgress()) return;
|
|
_running = true;
|
|
try {
|
|
const pending = await _listPendingAgentClientBuilds(1);
|
|
if (!pending.length) return;
|
|
const row = pending[0];
|
|
await db.upsertAgentBundleBuild({
|
|
brandingHash: row.branding_hash,
|
|
platform: row.platform,
|
|
arch: row.arch,
|
|
format: row.format,
|
|
status: 'building',
|
|
artifactPath: row.artifact_path || null,
|
|
artifactSize: row.artifact_size || 0,
|
|
artifactSha256: row.artifact_sha256 || null,
|
|
errorMessage: '',
|
|
});
|
|
_activeBuilds++;
|
|
try {
|
|
await _runOne(row);
|
|
} catch (e) {
|
|
const msg = e.message || String(e);
|
|
console.error(`[agentClientBuildWorker] build FAILED ${row.platform}/${row.format}: ${msg}`);
|
|
await db.upsertAgentBundleBuild({
|
|
brandingHash: row.branding_hash,
|
|
platform: row.platform,
|
|
arch: row.arch,
|
|
format: row.format,
|
|
status: 'failed',
|
|
artifactPath: null,
|
|
artifactSize: 0,
|
|
artifactSha256: null,
|
|
errorMessage: msg.slice(0, 2000),
|
|
});
|
|
} finally {
|
|
_activeBuilds--;
|
|
}
|
|
} finally {
|
|
_running = false;
|
|
}
|
|
}
|
|
|
|
function startWorker() {
|
|
if (_pollTimer) return;
|
|
if (!fs.existsSync(path.join(SOURCE_ROOT, 'src-tauri', 'Cargo.toml'))) {
|
|
console.warn('[agentClientBuildWorker] betterdesk-agent-client source not found — worker disabled');
|
|
return;
|
|
}
|
|
fsp.mkdir(WORK_ROOT, { recursive: true }).catch(() => {});
|
|
fsp.mkdir(ARTIFACT_ROOT, { recursive: true }).catch(() => {});
|
|
_pollTimer = setInterval(() => {
|
|
_tick().catch((e) => console.error('[agentClientBuildWorker] tick error:', e.message));
|
|
}, POLL_INTERVAL_MS);
|
|
console.log(`[agentClientBuildWorker] started source=${SOURCE_ROOT} poll=${POLL_INTERVAL_MS}ms`);
|
|
}
|
|
|
|
function stopWorker() {
|
|
if (_pollTimer) clearInterval(_pollTimer);
|
|
_pollTimer = null;
|
|
}
|
|
|
|
module.exports = {
|
|
startWorker,
|
|
stopWorker,
|
|
enqueueBuildsForHash,
|
|
requeueAllBundleBuilds,
|
|
rebuildBundleById,
|
|
requeuePlatformBuild,
|
|
_internals: {
|
|
isAgentClientBundle: _isAgentClientBundle,
|
|
listPendingBuilds: _listPendingAgentClientBuilds,
|
|
},
|
|
};
|