fix(install): preserve user fleet data on re-seed + refresh active units (CRITICAL) (#632)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #632.
This commit is contained in:
@@ -7,7 +7,9 @@ import {
|
||||
buildRelaunchCommands,
|
||||
readRosterAgentNames,
|
||||
runFrameworkReseed,
|
||||
refreshActiveFleetUnits,
|
||||
} from './update-checker.js';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
|
||||
/**
|
||||
* F3-m3 / R13: `mosaic update` re-seeds the framework + (opt-in) relaunches
|
||||
@@ -83,3 +85,41 @@ describe('runFrameworkReseed', () => {
|
||||
rmSync(missing, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshActiveFleetUnits', () => {
|
||||
let root: string;
|
||||
let mosaicHome: string;
|
||||
let configHome: string;
|
||||
|
||||
beforeEach(() => {
|
||||
root = mkdtempSync(join(tmpdir(), 'mosaic-units-'));
|
||||
mosaicHome = join(root, 'mosaic');
|
||||
configHome = join(root, 'config');
|
||||
mkdirSync(join(mosaicHome, 'systemd', 'user'), { recursive: true });
|
||||
mkdirSync(join(configHome, 'systemd', 'user'), { recursive: true });
|
||||
// Freshly re-seeded units (new content).
|
||||
writeFileSync(join(mosaicHome, 'systemd', 'user', 'mosaic-agent@.service'), 'NEW\n');
|
||||
writeFileSync(join(mosaicHome, 'systemd', 'user', 'mosaic-tmux-holder.service'), 'NEW\n');
|
||||
});
|
||||
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
||||
|
||||
it('refreshes active units when a fleet is already installed', () => {
|
||||
// Active dir already carries mosaic units (stale) → fleet is installed.
|
||||
writeFileSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'), 'OLD\n');
|
||||
const res = refreshActiveFleetUnits(mosaicHome, {
|
||||
XDG_CONFIG_HOME: configHome,
|
||||
} as NodeJS.ProcessEnv);
|
||||
expect(res.refreshed).toContain('mosaic-agent@.service');
|
||||
expect(
|
||||
readFileSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'), 'utf-8'),
|
||||
).toBe('NEW\n');
|
||||
});
|
||||
|
||||
it('is a no-op when no fleet is installed (active dir has no mosaic units)', () => {
|
||||
const res = refreshActiveFleetUnits(mosaicHome, {
|
||||
XDG_CONFIG_HOME: configHome,
|
||||
} as NodeJS.ProcessEnv);
|
||||
expect(res.refreshed).toEqual([]);
|
||||
expect(existsSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,14 @@
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
readdirSync,
|
||||
copyFileSync,
|
||||
} from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -536,6 +543,47 @@ export function readRosterAgentNames(mosaicHome = join(homedir(), '.config', 'mo
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the ACTIVE systemd user units from the freshly re-seeded copies.
|
||||
*
|
||||
* The re-seed updates `~/.config/mosaic/systemd/user/*.service`, but the units
|
||||
* systemd actually runs live at `~/.config/systemd/user/`. Without this copy,
|
||||
* shipped unit fixes (e.g. the socket-env change) never take effect after
|
||||
* `mosaic update` until `mosaic fleet install` is re-run. Best-effort + scoped:
|
||||
* only refreshes when a fleet is already installed (the active dir already
|
||||
* carries `mosaic-*` units), so non-fleet hosts are untouched.
|
||||
*/
|
||||
export function refreshActiveFleetUnits(
|
||||
mosaicHome = join(homedir(), '.config', 'mosaic'),
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): { refreshed: string[]; ok: boolean; reason?: string } {
|
||||
const src = join(mosaicHome, 'systemd', 'user');
|
||||
const configHome = env['XDG_CONFIG_HOME'] ?? join(homedir(), '.config');
|
||||
const dest = join(configHome, 'systemd', 'user');
|
||||
if (!existsSync(src)) return { refreshed: [], ok: true };
|
||||
// Only refresh when a fleet is already installed (active dir has mosaic units).
|
||||
const fleetInstalled =
|
||||
existsSync(dest) &&
|
||||
readdirSync(dest).some((f) => f.startsWith('mosaic-') && f.endsWith('.service'));
|
||||
if (!fleetInstalled) return { refreshed: [], ok: true };
|
||||
const units = readdirSync(src).filter((f) => f.startsWith('mosaic-') && f.endsWith('.service'));
|
||||
const refreshed: string[] = [];
|
||||
for (const unit of units) {
|
||||
try {
|
||||
copyFileSync(join(src, unit), join(dest, unit));
|
||||
refreshed.push(unit);
|
||||
} catch {
|
||||
// best-effort per unit
|
||||
}
|
||||
}
|
||||
try {
|
||||
execSync('systemctl --user daemon-reload', { stdio: 'ignore', timeout: 15_000 });
|
||||
} catch {
|
||||
// non-systemd host or no session bus — non-fatal
|
||||
}
|
||||
return { refreshed, ok: true };
|
||||
}
|
||||
|
||||
/** Build the per-agent systemd relaunch commands (drain+relaunch via restart). */
|
||||
export function buildRelaunchCommands(agentNames: string[]): string[][] {
|
||||
return agentNames.map((name) => [
|
||||
|
||||
Reference in New Issue
Block a user