dsh-home-hosted 0.4.0 → 0.4.2
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.
- package/docs/DESIGN.md +32 -7
- package/lib/client.js +6 -6
- package/lib/index.js +203 -44
- package/lib/index.js.map +3 -3
- package/lib/types/home-hosted/dsh-entry.d.ts +45 -2
- package/lib/types/home-hosted/entries.d.ts +7 -0
- package/lib/types/home-hosted/launch.d.ts +4 -0
- package/lib/types/service.d.ts +32 -0
- package/lib/types/util/paths.d.ts +7 -8
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1974,39 +1974,53 @@ function expandHome(value) {
|
|
|
1974
1974
|
return path6.join(os3.homedir(), value.slice(2));
|
|
1975
1975
|
return value;
|
|
1976
1976
|
}
|
|
1977
|
+
function resolveUnder(value, homeDir) {
|
|
1978
|
+
if (value === "~")
|
|
1979
|
+
return path6.resolve(homeDir);
|
|
1980
|
+
if (value.startsWith("~/") || value.startsWith("~\\"))
|
|
1981
|
+
return path6.resolve(path6.join(homeDir, value.slice(2)));
|
|
1982
|
+
return path6.isAbsolute(value) ? path6.resolve(value) : path6.resolve(homeDir, value);
|
|
1983
|
+
}
|
|
1977
1984
|
function dshHome() {
|
|
1978
1985
|
const configured = process.env.DSH_HOME?.trim();
|
|
1979
1986
|
return configured ? path6.resolve(expandHome(configured)) : path6.join(os3.homedir(), ".dsh");
|
|
1980
1987
|
}
|
|
1981
|
-
function homeHostedHome() {
|
|
1982
|
-
const configured = process.env.HHOSTED_HOME?.trim();
|
|
1983
|
-
return configured ? path6.resolve(expandHome(configured)) : path6.join(os3.homedir(), ".home-hosted");
|
|
1984
|
-
}
|
|
1985
1988
|
function resolveHomeHostedHome(stateDir, env = process.env, homeDir = os3.homedir()) {
|
|
1986
1989
|
const configured = env.HHOSTED_HOME?.trim();
|
|
1987
1990
|
if (configured)
|
|
1988
|
-
return { home:
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1991
|
+
return { home: resolveUnder(configured, homeDir), source: "env" };
|
|
1992
|
+
const legacy = path6.join(homeDir, ".home-hosted");
|
|
1993
|
+
if (hasLivePanel(legacy))
|
|
1994
|
+
return { home: legacy, source: "legacy" };
|
|
1995
|
+
return { home: path6.join(resolveUnder(stateDir, homeDir), "panel"), source: "instance" };
|
|
1992
1996
|
}
|
|
1993
|
-
function
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
return
|
|
1997
|
-
|
|
1997
|
+
function hasLivePanel(home) {
|
|
1998
|
+
try {
|
|
1999
|
+
if (fs7.statSync(path6.join(home, "servers.config.json")).isFile())
|
|
2000
|
+
return true;
|
|
2001
|
+
} catch {
|
|
2002
|
+
}
|
|
2003
|
+
try {
|
|
2004
|
+
const raw = JSON.parse(fs7.readFileSync(path6.join(home, "run.json"), "utf8"));
|
|
2005
|
+
if (typeof raw.pid !== "number")
|
|
1998
2006
|
return false;
|
|
1999
|
-
|
|
2000
|
-
|
|
2007
|
+
process.kill(raw.pid, 0);
|
|
2008
|
+
return true;
|
|
2009
|
+
} catch {
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2001
2012
|
}
|
|
2002
2013
|
function bootUnitName(stateDir) {
|
|
2003
|
-
if (
|
|
2014
|
+
if (isMachineDefaultStateDir(stateDir))
|
|
2004
2015
|
return "home-hosted";
|
|
2005
|
-
|
|
2016
|
+
const key = path6.resolve(expandHome(stateDir?.trim() ?? ""));
|
|
2017
|
+
return `home-hosted-${crypto.createHash("sha256").update(key).digest("hex").slice(0, 8)}`;
|
|
2006
2018
|
}
|
|
2007
|
-
function
|
|
2019
|
+
function isMachineDefaultStateDir(stateDir) {
|
|
2008
2020
|
const bare = stateDir?.trim();
|
|
2009
|
-
|
|
2021
|
+
if (bare === void 0 || bare.length === 0)
|
|
2022
|
+
return true;
|
|
2023
|
+
return path6.resolve(expandHome(bare)) === path6.join(os3.homedir(), ".dsh", "dsh-home-hosted");
|
|
2010
2024
|
}
|
|
2011
2025
|
function pluginStateDir(override) {
|
|
2012
2026
|
const configured = override?.trim();
|
|
@@ -2014,13 +2028,13 @@ function pluginStateDir(override) {
|
|
|
2014
2028
|
return path6.resolve(expandHome(configured));
|
|
2015
2029
|
return path6.join(dshHome(), "dsh-home-hosted");
|
|
2016
2030
|
}
|
|
2017
|
-
function runtimeFile(home
|
|
2031
|
+
function runtimeFile(home) {
|
|
2018
2032
|
return path6.join(home, "run.json");
|
|
2019
2033
|
}
|
|
2020
|
-
function secretsFile(home
|
|
2034
|
+
function secretsFile(home) {
|
|
2021
2035
|
return path6.join(home, ".control-secrets.json");
|
|
2022
2036
|
}
|
|
2023
|
-
function configFile(home
|
|
2037
|
+
function configFile(home) {
|
|
2024
2038
|
return path6.join(home, "servers.config.json");
|
|
2025
2039
|
}
|
|
2026
2040
|
|
|
@@ -2081,6 +2095,7 @@ import process7 from "node:process";
|
|
|
2081
2095
|
import fs9 from "node:fs";
|
|
2082
2096
|
import path7 from "node:path";
|
|
2083
2097
|
import process4 from "node:process";
|
|
2098
|
+
var LEGACY_BOOT_UNIT_NAME = "home-hosted";
|
|
2084
2099
|
async function which(command) {
|
|
2085
2100
|
const probe = process4.platform === "win32" ? "where.exe" : "which";
|
|
2086
2101
|
const result = await run(probe, [command], { timeoutMs: 5e3 });
|
|
@@ -2138,8 +2153,8 @@ function buildHomeHostedBootSpec(facts, launch, options) {
|
|
|
2138
2153
|
env: homeHostedEnv(facts, launch),
|
|
2139
2154
|
marker: options.marker ?? "managed by dsh-home-hosted",
|
|
2140
2155
|
// One artifact per state root, so two plugin instances cannot overwrite or
|
|
2141
|
-
// delete each other's entry. The default install keeps the
|
|
2142
|
-
unitName: bootUnitName(options.stateDir),
|
|
2156
|
+
// delete each other's entry. The machine-default install keeps the old name.
|
|
2157
|
+
unitName: options.unitName ?? bootUnitName(options.stateDir),
|
|
2143
2158
|
label: "home-hosted control panel",
|
|
2144
2159
|
logDir
|
|
2145
2160
|
};
|
|
@@ -2836,10 +2851,48 @@ function readdirNames(dir) {
|
|
|
2836
2851
|
return [];
|
|
2837
2852
|
}
|
|
2838
2853
|
}
|
|
2854
|
+
function projectDshEntry(dir, depth = 4) {
|
|
2855
|
+
let current = resolveDir(dir);
|
|
2856
|
+
for (let hop = 0; hop <= depth; hop += 1) {
|
|
2857
|
+
const shim = path10.join(current, "node_modules", ".bin", "dsh");
|
|
2858
|
+
const shimmed = resolveShimmedCli(shim, process7.execPath);
|
|
2859
|
+
if (shimmed?.cliEntry != null && isEntry(shimmed.cliEntry))
|
|
2860
|
+
return shimmed.cliEntry;
|
|
2861
|
+
const found = dshCandidatesUnder(current).map((entry) => ({ entry, version: versionOf(entry) })).sort((a, b) => compareVersions(b.version ?? "0.0.0", a.version ?? "0.0.0"));
|
|
2862
|
+
if (found[0] !== void 0)
|
|
2863
|
+
return found[0].entry;
|
|
2864
|
+
const parent = path10.dirname(current);
|
|
2865
|
+
if (parent === current)
|
|
2866
|
+
break;
|
|
2867
|
+
current = parent;
|
|
2868
|
+
}
|
|
2869
|
+
return null;
|
|
2870
|
+
}
|
|
2871
|
+
function resolveDir(dir) {
|
|
2872
|
+
try {
|
|
2873
|
+
return fs12.realpathSync(dir);
|
|
2874
|
+
} catch {
|
|
2875
|
+
return path10.resolve(dir);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2839
2878
|
async function resolveDshLaunch(options = {}) {
|
|
2840
2879
|
const argv1 = options.argv1 === void 0 ? process7.argv[1] : options.argv1;
|
|
2841
2880
|
const dshHome2 = options.dshHome ?? dshHome();
|
|
2842
2881
|
const stateDir = options.stateDir ?? pluginStateDir();
|
|
2882
|
+
const projectDir = options.projectDir === void 0 ? process7.cwd() : options.projectDir;
|
|
2883
|
+
const declared = projectDir === null || projectDir.trim().length === 0 ? null : projectDshEntry(projectDir);
|
|
2884
|
+
if (declared !== null && realPath(declared) !== realPath(launcherFor(stateDir, declared))) {
|
|
2885
|
+
const launcher = writeDshLauncher({
|
|
2886
|
+
stateDir,
|
|
2887
|
+
dshHome: dshHome2,
|
|
2888
|
+
resolvedEntry: declared,
|
|
2889
|
+
pinnedEntry: declared,
|
|
2890
|
+
repin: true,
|
|
2891
|
+
entryExtension: path10.extname(declared),
|
|
2892
|
+
searchRoots: [dshSearchRoot(declared), projectDir].filter((root) => typeof root === "string" && root.length > 0)
|
|
2893
|
+
});
|
|
2894
|
+
return { program: process7.execPath, args: [declared], cliEntry: declared, shimPath: null, source: "entry", launcherPath: launcher.path };
|
|
2895
|
+
}
|
|
2843
2896
|
const local = typeof argv1 === "string" && argv1.length > 0 ? launchedEntry(argv1) : null;
|
|
2844
2897
|
if (local !== null && realPath(local) !== realPath(dshLauncherPath(stateDir, path10.extname(local)))) {
|
|
2845
2898
|
const launcher = writeDshLauncher({
|
|
@@ -2880,6 +2933,9 @@ async function resolveDshLaunch(options = {}) {
|
|
|
2880
2933
|
const launch = resolveShimmedCli(found, process7.execPath);
|
|
2881
2934
|
return launch === null ? null : { ...launch, launcherPath: null };
|
|
2882
2935
|
}
|
|
2936
|
+
function launcherFor(stateDir, entry) {
|
|
2937
|
+
return dshLauncherPath(stateDir, path10.extname(entry));
|
|
2938
|
+
}
|
|
2883
2939
|
function buildCommand(launch, launcherPath2) {
|
|
2884
2940
|
if (launcherPath2 !== null && launcherPath2.length > 0)
|
|
2885
2941
|
return { command: process7.execPath, entryArgs: [launcherPath2] };
|
|
@@ -2894,13 +2950,24 @@ function entryScript(config) {
|
|
|
2894
2950
|
const rest = config.args ?? [];
|
|
2895
2951
|
return rest.find((arg) => path10.isAbsolute(arg)) ?? program;
|
|
2896
2952
|
}
|
|
2897
|
-
function
|
|
2953
|
+
function localDshCommand(projectDir, entry) {
|
|
2954
|
+
if (typeof projectDir !== "string" || projectDir.trim().length === 0 || entry === null)
|
|
2955
|
+
return null;
|
|
2956
|
+
const shim = resolveShimmedCli(path10.join(projectDir, "node_modules", ".bin", "dsh"))?.cliEntry ?? null;
|
|
2957
|
+
if (shim === null)
|
|
2958
|
+
return null;
|
|
2959
|
+
return realPath(shim) === realPath(entry) ? "dsh" : null;
|
|
2960
|
+
}
|
|
2961
|
+
function pinnedScriptFor(launch, projectDir) {
|
|
2898
2962
|
if (launch === null)
|
|
2899
2963
|
return null;
|
|
2964
|
+
const entry = launch.cliEntry ?? launch.args.find((arg) => path10.isAbsolute(arg)) ?? null;
|
|
2965
|
+
if (localDshCommand(projectDir, entry) === "dsh")
|
|
2966
|
+
return "dsh";
|
|
2900
2967
|
const launcher = launch.launcherPath;
|
|
2901
2968
|
if (launcher !== null && launcher.length > 0)
|
|
2902
2969
|
return launcher;
|
|
2903
|
-
return
|
|
2970
|
+
return entry;
|
|
2904
2971
|
}
|
|
2905
2972
|
function isBareCommand(stored) {
|
|
2906
2973
|
return !/[/\\]/.test(stored) && (stored === "dsh" || /^dsh\.(?:cmd|exe)$/i.test(stored));
|
|
@@ -2915,23 +2982,24 @@ function needsLauncherRepair(config) {
|
|
|
2915
2982
|
return true;
|
|
2916
2983
|
return (config.args ?? []).some((arg) => path10.isAbsolute(arg));
|
|
2917
2984
|
}
|
|
2918
|
-
function launcherRepair(config, launch) {
|
|
2919
|
-
const pinned = pinnedScriptFor(launch);
|
|
2985
|
+
function launcherRepair(config, launch, options = {}) {
|
|
2986
|
+
const pinned = pinnedScriptFor(launch, options.projectDir);
|
|
2920
2987
|
if (pinned === null || launch === null)
|
|
2921
2988
|
return null;
|
|
2989
|
+
const targetBare = pinned === "dsh";
|
|
2990
|
+
const storedBare = isBareCommand(entryScript(config) ?? "");
|
|
2922
2991
|
const stored = entryScript(config);
|
|
2923
2992
|
if (stored === null || stored === pinned)
|
|
2924
2993
|
return null;
|
|
2925
|
-
|
|
2926
|
-
if (!bare) {
|
|
2994
|
+
if (!storedBare && options.ownedCommand !== true) {
|
|
2927
2995
|
const target = resolveShimmedCli(stored, process7.execPath)?.cliEntry ?? stored;
|
|
2928
|
-
if (target !==
|
|
2996
|
+
if (target !== launch.cliEntry && target !== pinned)
|
|
2929
2997
|
return null;
|
|
2930
2998
|
}
|
|
2931
2999
|
const args = config.args ?? [];
|
|
2932
|
-
const
|
|
2933
|
-
const rest =
|
|
2934
|
-
return { command: process7.execPath, args: [pinned, ...rest] };
|
|
3000
|
+
const script = storedBare ? -1 : args.findIndex((arg) => path10.isAbsolute(arg) || arg === pinned);
|
|
3001
|
+
const rest = script === -1 ? args : args.filter((_, index) => index !== script);
|
|
3002
|
+
return targetBare ? { command: "dsh", args: rest, ...typeof options.projectDir === "string" && options.projectDir.length > 0 ? { cwd: options.projectDir } : {} } : { command: process7.execPath, args: [pinned, ...rest] };
|
|
2935
3003
|
}
|
|
2936
3004
|
function buildDshEntry(facts) {
|
|
2937
3005
|
const launch = facts.launch;
|
|
@@ -2949,7 +3017,9 @@ function buildDshEntry(facts) {
|
|
|
2949
3017
|
);
|
|
2950
3018
|
}
|
|
2951
3019
|
const launcherArgs = profile === "web" ? ["web"] : ["--profile", profile];
|
|
2952
|
-
const
|
|
3020
|
+
const projectDir = facts.projectDir === void 0 || facts.projectDir === null ? process7.cwd() : facts.projectDir;
|
|
3021
|
+
const local = launch === null ? null : localDshCommand(projectDir, launch.cliEntry);
|
|
3022
|
+
const generated = launch === null || local === "dsh" ? { command: "dsh", entryArgs: [] } : buildCommand(launch, facts.launcherPath ?? null);
|
|
2953
3023
|
return {
|
|
2954
3024
|
id: facts.id,
|
|
2955
3025
|
label: "DSH web",
|
|
@@ -2959,7 +3029,7 @@ function buildDshEntry(facts) {
|
|
|
2959
3029
|
args: [...generated.entryArgs, ...launcherArgs, ...appArgs],
|
|
2960
3030
|
bind: "local",
|
|
2961
3031
|
port: facts.port,
|
|
2962
|
-
cwd:
|
|
3032
|
+
cwd: projectDir,
|
|
2963
3033
|
env: {},
|
|
2964
3034
|
dataEnvs: { DSH_HOME: facts.dshHome },
|
|
2965
3035
|
onPortConflict: "kill",
|
|
@@ -3190,6 +3260,11 @@ function ownedDrift(live, intent) {
|
|
|
3190
3260
|
drift.push("stop.killPortHolders");
|
|
3191
3261
|
return drift;
|
|
3192
3262
|
}
|
|
3263
|
+
function isCreatedEntry(snapshot) {
|
|
3264
|
+
if (snapshot === void 0)
|
|
3265
|
+
return false;
|
|
3266
|
+
return Object.keys(snapshot).length === 1 && typeof snapshot.id === "string";
|
|
3267
|
+
}
|
|
3193
3268
|
function snapshotOwned(live) {
|
|
3194
3269
|
const killPortHolders = live.stop?.killPortHolders;
|
|
3195
3270
|
return {
|
|
@@ -3231,7 +3306,7 @@ async function runCli(deps, command) {
|
|
|
3231
3306
|
return await deps.exec(args, deps.env);
|
|
3232
3307
|
const launch = deps.launch;
|
|
3233
3308
|
if (launch === null)
|
|
3234
|
-
return { code: null, stdout: "", stderr: "no home-hosted CLI is available" };
|
|
3309
|
+
return { code: null, stdout: "", stderr: "no home-hosted CLI is available", error: null };
|
|
3235
3310
|
return await run(launch.program, args, {
|
|
3236
3311
|
env: deps.env,
|
|
3237
3312
|
timeoutMs: deps.timeoutMs ?? 9e4
|
|
@@ -3249,7 +3324,7 @@ async function startPanel(deps) {
|
|
|
3249
3324
|
async function stopPanel(deps) {
|
|
3250
3325
|
const result = await runCli(deps, "down");
|
|
3251
3326
|
if (result.code !== 0) {
|
|
3252
|
-
const detail = result.stderr.trim() || result.stdout.trim() || `the CLI exited ${String(result.code)}
|
|
3327
|
+
const detail = result.error ?? (result.stderr.trim() || result.stdout.trim() || `the CLI exited ${String(result.code)}`);
|
|
3253
3328
|
return { ok: false, detail: `the panel did not stop: ${detail}` };
|
|
3254
3329
|
}
|
|
3255
3330
|
return { ok: true, detail: "the panel stopped; the servers it supervised stopped with it" };
|
|
@@ -3573,13 +3648,19 @@ var HomeHostedService = class extends Service {
|
|
|
3573
3648
|
tokenDetail = "";
|
|
3574
3649
|
/** When a missing managed entry was last put back, so a poll cannot become a write loop. */
|
|
3575
3650
|
entryRecoveryAt = 0;
|
|
3651
|
+
/** When a managed row was last re-pointed at the pinned dsh, for the same reason. */
|
|
3652
|
+
commandRepairAt = 0;
|
|
3576
3653
|
/** The last panel inventory, so a model-step assembly never reads the disk. */
|
|
3577
3654
|
instanceCache = null;
|
|
3578
3655
|
snapshotsFile;
|
|
3579
|
-
|
|
3656
|
+
projectDir() {
|
|
3657
|
+
return this.options.projectDir ?? process10.cwd();
|
|
3658
|
+
}
|
|
3659
|
+
async resolveDsh(dshHome2, projectDir = this.projectDir()) {
|
|
3580
3660
|
return await (this.options.resolveDsh ?? ((options) => resolveDshLaunch(options)))({
|
|
3581
3661
|
dshHome: dshHome2,
|
|
3582
|
-
stateDir: this.options.stateDir
|
|
3662
|
+
stateDir: this.options.stateDir,
|
|
3663
|
+
projectDir
|
|
3583
3664
|
});
|
|
3584
3665
|
}
|
|
3585
3666
|
// -------------------------------------------------------------------------
|
|
@@ -4015,7 +4096,8 @@ var HomeHostedService = class extends Service {
|
|
|
4015
4096
|
profile: detectProfile(process10.argv, process10.env),
|
|
4016
4097
|
dshHome: harnessHome,
|
|
4017
4098
|
launch: dsh,
|
|
4018
|
-
launcherPath: dsh?.launcherPath ?? null
|
|
4099
|
+
launcherPath: dsh?.launcherPath ?? null,
|
|
4100
|
+
projectDir: this.projectDir()
|
|
4019
4101
|
});
|
|
4020
4102
|
return { ...generated, ...patch };
|
|
4021
4103
|
}
|
|
@@ -4030,7 +4112,8 @@ var HomeHostedService = class extends Service {
|
|
|
4030
4112
|
return null;
|
|
4031
4113
|
const harnessHome = process10.env.DSH_HOME ?? dshHome();
|
|
4032
4114
|
const dsh = await this.resolveDsh(harnessHome);
|
|
4033
|
-
|
|
4115
|
+
const owned = isCreatedEntry(this.snapshots()[live.id]);
|
|
4116
|
+
return launcherRepair(live, dsh, { ownedCommand: owned, projectDir: this.projectDir() });
|
|
4034
4117
|
}
|
|
4035
4118
|
async writeOwned(intent) {
|
|
4036
4119
|
const live = (await this.liveEntries()).get(intent.id) ?? null;
|
|
@@ -4270,7 +4353,7 @@ var HomeHostedService = class extends Service {
|
|
|
4270
4353
|
version: runtime?.version ?? null
|
|
4271
4354
|
},
|
|
4272
4355
|
launch,
|
|
4273
|
-
{ stateDir: this.options.stateDir }
|
|
4356
|
+
{ stateDir: this.options.stateDir, unitName: bootUnitName(this.options.stateDir) }
|
|
4274
4357
|
);
|
|
4275
4358
|
if (launcher !== null) {
|
|
4276
4359
|
const cliArgs2 = spec.args.slice(launch.args.length);
|
|
@@ -4310,6 +4393,8 @@ var HomeHostedService = class extends Service {
|
|
|
4310
4393
|
if (spec === null)
|
|
4311
4394
|
throw new HomeHostedError((await this.cli()).resolution.status.detail, "CLI_NOT_FOUND");
|
|
4312
4395
|
const result = await this.ladder().install(spec, mechanism);
|
|
4396
|
+
if (result.ok)
|
|
4397
|
+
await this.retireLegacyBootEntry(spec, mechanism);
|
|
4313
4398
|
const current = this.options.settings.get().autostart;
|
|
4314
4399
|
this.options.settings.update({
|
|
4315
4400
|
autostart: {
|
|
@@ -4334,6 +4419,8 @@ var HomeHostedService = class extends Service {
|
|
|
4334
4419
|
if (spec === null)
|
|
4335
4420
|
throw new HomeHostedError((await this.cli()).resolution.status.detail, "CLI_NOT_FOUND");
|
|
4336
4421
|
const result = await this.ladder().uninstall(spec, mechanism);
|
|
4422
|
+
if (result.ok)
|
|
4423
|
+
await this.retireLegacyBootEntry(spec, mechanism);
|
|
4337
4424
|
const current = this.options.settings.get().autostart;
|
|
4338
4425
|
this.options.settings.update({
|
|
4339
4426
|
autostart: {
|
|
@@ -4351,6 +4438,28 @@ var HomeHostedService = class extends Service {
|
|
|
4351
4438
|
});
|
|
4352
4439
|
return { result, status: await this.bootStatus(mechanism) };
|
|
4353
4440
|
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Remove the pre-per-instance artifact once this state root has its own.
|
|
4443
|
+
*
|
|
4444
|
+
* An install that used to write `home-hosted` now writes `home-hosted-<hash>`,
|
|
4445
|
+
* and a machine left with both would autostart the panel twice — or keep
|
|
4446
|
+
* starting it from an entry the page no longer reports. Only an artifact
|
|
4447
|
+
* carrying this plugin's marker is touched, so somebody else's unit survives.
|
|
4448
|
+
*/
|
|
4449
|
+
async retireLegacyBootEntry(spec, mechanism) {
|
|
4450
|
+
if (spec.unitName === LEGACY_BOOT_UNIT_NAME)
|
|
4451
|
+
return;
|
|
4452
|
+
const legacy = { ...spec, unitName: LEGACY_BOOT_UNIT_NAME };
|
|
4453
|
+
try {
|
|
4454
|
+
const status = await this.ladder().status(legacy, mechanism);
|
|
4455
|
+
if (status.unitPath === null || status.state === "not-installed" || status.state === "unsupported")
|
|
4456
|
+
return;
|
|
4457
|
+
if (!fs15.existsSync(status.unitPath))
|
|
4458
|
+
return;
|
|
4459
|
+
await this.ladder().uninstall(legacy, status.mechanism ?? mechanism);
|
|
4460
|
+
} catch {
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4354
4463
|
// -------------------------------------------------------------------------
|
|
4355
4464
|
// Panel lifecycle
|
|
4356
4465
|
// -------------------------------------------------------------------------
|
|
@@ -4436,7 +4545,29 @@ var HomeHostedService = class extends Service {
|
|
|
4436
4545
|
*/
|
|
4437
4546
|
async reconcile() {
|
|
4438
4547
|
await this.ensureManagedEntry();
|
|
4548
|
+
await this.repairManagedCommand();
|
|
4439
4549
|
await this.repairBootEntry();
|
|
4550
|
+
await this.migrateBootEntryName();
|
|
4551
|
+
}
|
|
4552
|
+
/**
|
|
4553
|
+
* An upgrade from a release that named every artifact `home-hosted` leaves
|
|
4554
|
+
* that one behind while this state root now writes its own name. Once our own
|
|
4555
|
+
* artifact is in place, the old one is retired — otherwise the machine
|
|
4556
|
+
* autostarts the panel twice, or from an entry the page no longer reports.
|
|
4557
|
+
*/
|
|
4558
|
+
async migrateBootEntryName() {
|
|
4559
|
+
if (!this.options.settings.get().autostart.enabled)
|
|
4560
|
+
return;
|
|
4561
|
+
const spec = await this.bootSpec();
|
|
4562
|
+
if (spec === null || spec.unitName === LEGACY_BOOT_UNIT_NAME)
|
|
4563
|
+
return;
|
|
4564
|
+
try {
|
|
4565
|
+
const own = await this.bootStatus();
|
|
4566
|
+
if (own.state !== "enabled-running" && own.state !== "enabled-failing" && own.state !== "installed-disabled")
|
|
4567
|
+
return;
|
|
4568
|
+
await this.retireLegacyBootEntry(spec, own.mechanism ?? void 0);
|
|
4569
|
+
} catch {
|
|
4570
|
+
}
|
|
4440
4571
|
}
|
|
4441
4572
|
/**
|
|
4442
4573
|
* Put back a managed entry that no longer exists, while its intent still wants
|
|
@@ -4463,6 +4594,34 @@ var HomeHostedService = class extends Service {
|
|
|
4463
4594
|
} catch {
|
|
4464
4595
|
}
|
|
4465
4596
|
}
|
|
4597
|
+
/**
|
|
4598
|
+
* Re-point a managed `dsh` row that runs the wrong dsh.
|
|
4599
|
+
*
|
|
4600
|
+
* A row created before the launcher existed, or by an older release that took
|
|
4601
|
+
* whatever PATH answered, keeps starting that copy — and no click would fix
|
|
4602
|
+
* it, because the plugin only writes the row when the toggle is applied. So
|
|
4603
|
+
* the drift is repaired where the entry's existence is already checked, under
|
|
4604
|
+
* the same throttle, and only for a row this plugin owns.
|
|
4605
|
+
*/
|
|
4606
|
+
async repairManagedCommand() {
|
|
4607
|
+
const id = this.options.defaultEntryId;
|
|
4608
|
+
if (!this.options.settings.get().manageDsh || !this.options.settings.intentFor(id).autostart)
|
|
4609
|
+
return;
|
|
4610
|
+
const now = Date.now();
|
|
4611
|
+
if (now - this.commandRepairAt < ENTRY_RECOVERY_INTERVAL_MS)
|
|
4612
|
+
return;
|
|
4613
|
+
const live = (await this.liveEntries()).get(id)?.config ?? null;
|
|
4614
|
+
if (live === null || this.snapshots()[id] === void 0 || !needsLauncherRepair(live))
|
|
4615
|
+
return;
|
|
4616
|
+
const repair = await this.dshCommandRepair(live);
|
|
4617
|
+
if (repair === null)
|
|
4618
|
+
return;
|
|
4619
|
+
this.commandRepairAt = now;
|
|
4620
|
+
try {
|
|
4621
|
+
await this.writeOwned({ ...this.options.settings.intentFor(id), autostart: true });
|
|
4622
|
+
} catch {
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4466
4625
|
/** Re-assert the boot entry when the OS still has it but it stopped working. */
|
|
4467
4626
|
async repairBootEntry() {
|
|
4468
4627
|
if (!this.options.settings.get().autostart.enabled)
|
|
@@ -4516,7 +4675,7 @@ var HomeHostedService = class extends Service {
|
|
|
4516
4675
|
boot: await this.bootStatus(),
|
|
4517
4676
|
// A deleted managed entry is put back here as well as at startup: whoever
|
|
4518
4677
|
// deleted it is looking at the page that reports it missing.
|
|
4519
|
-
entries: await this.ensureManagedEntry().then(async () => await this.entriesStatus()),
|
|
4678
|
+
entries: await this.ensureManagedEntry().then(async () => await this.repairManagedCommand()).then(async () => await this.entriesStatus()),
|
|
4520
4679
|
servers,
|
|
4521
4680
|
settings: this.options.settings.get(),
|
|
4522
4681
|
cli,
|