kablan 0.9.0 → 0.9.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/bin/cli.js +9 -4
- package/bin/install-app.js +93 -4
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -148,10 +148,12 @@ async function main() {
|
|
|
148
148
|
const args = process.argv.slice(2);
|
|
149
149
|
const isMcpMode = args.includes("--mcp");
|
|
150
150
|
|
|
151
|
-
// `--install` puts a double-clickable app in ~/Applications and exits
|
|
152
|
-
//
|
|
151
|
+
// `--install` puts a double-clickable app in ~/Applications and exits. It stops a running
|
|
152
|
+
// Kablan first: the swap would otherwise leave the old process serving, and the update would
|
|
153
|
+
// look like it had not happened.
|
|
153
154
|
if (args.includes("--uninstall")) {
|
|
154
|
-
const removed = uninstallMacApp();
|
|
155
|
+
const { app: removed, stoppedPid } = uninstallMacApp();
|
|
156
|
+
if (stoppedPid) console.log(`Stopped the running Kablan (pid ${stoppedPid})`);
|
|
155
157
|
console.log(removed ? `Removed ${removed}` : `Nothing installed at ${appDir()}`);
|
|
156
158
|
console.log("Cached binaries are still in ~/.kablan/bin; delete that to reclaim the space.");
|
|
157
159
|
return;
|
|
@@ -159,7 +161,10 @@ async function main() {
|
|
|
159
161
|
|
|
160
162
|
if (args.includes("--install")) {
|
|
161
163
|
await extractAndRun("kablan", (bin) => {
|
|
162
|
-
const app = installMacApp(bin, CLI_VERSION);
|
|
164
|
+
const { app, stoppedPid } = installMacApp(bin, CLI_VERSION);
|
|
165
|
+
if (stoppedPid) {
|
|
166
|
+
console.log(`\nStopped the running Kablan (pid ${stoppedPid}) so this version replaces it`);
|
|
167
|
+
}
|
|
163
168
|
console.log(`\nInstalled ${app}`);
|
|
164
169
|
console.log("Open it from Launchpad or Spotlight — it starts Kablan in the background");
|
|
165
170
|
console.log("and opens your browser. Logs: ~/Library/Logs/Kablan/kablan.log");
|
package/bin/install-app.js
CHANGED
|
@@ -111,9 +111,92 @@ function infoPlist(version) {
|
|
|
111
111
|
`;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Stop a Kablan that is already running, and say whether one was.
|
|
117
|
+
*
|
|
118
|
+
* Replacing the bundle under a live process does nothing to that process: it keeps serving, the
|
|
119
|
+
* launcher finds its port file and simply opens a browser at it, and the update looks like it
|
|
120
|
+
* failed — the app still reports the version it was started with. So the install stops it first.
|
|
121
|
+
*
|
|
122
|
+
* The port file is the only handle we have on it. Whoever is listening on that port has to look
|
|
123
|
+
* like Kablan before it is signalled, because a stale port file plus a recycled port would
|
|
124
|
+
* otherwise point at some unrelated process. SIGTERM first so the server can close its database
|
|
125
|
+
* cleanly; SIGKILL only if it is still there after a grace period.
|
|
126
|
+
*
|
|
127
|
+
* @returns the pid stopped, or null when nothing was running
|
|
128
|
+
*/
|
|
129
|
+
function stopRunningApp() {
|
|
130
|
+
const bin = APP_NAME.toLowerCase();
|
|
131
|
+
const tmp = process.env.TMPDIR || "/tmp";
|
|
132
|
+
const portFile = path.join(tmp, bin, `${bin}.port`);
|
|
133
|
+
|
|
134
|
+
let port;
|
|
135
|
+
try {
|
|
136
|
+
port = fs.readFileSync(portFile, "utf8").trim();
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
if (!/^\d+$/.test(port)) return null;
|
|
141
|
+
|
|
142
|
+
let pids;
|
|
143
|
+
try {
|
|
144
|
+
pids = execFileSync("/usr/sbin/lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], {
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
147
|
+
})
|
|
148
|
+
.split("\n")
|
|
149
|
+
.map((line) => line.trim())
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
} catch {
|
|
152
|
+
// Nothing listening: the port file outlived the process it described.
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const pid of pids) {
|
|
157
|
+
let command = "";
|
|
158
|
+
try {
|
|
159
|
+
command = execFileSync("/bin/ps", ["-o", "command=", "-p", pid], {
|
|
160
|
+
encoding: "utf8",
|
|
161
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
162
|
+
}).trim();
|
|
163
|
+
} catch {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Ours: either a cached release binary or the one inside the bundle.
|
|
167
|
+
if (!command.includes(`.${bin}/bin/`) && !command.includes(`${bin}-server`)) continue;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
process.kill(Number(pid), "SIGTERM");
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Give it a moment to close its database before resorting to SIGKILL.
|
|
176
|
+
const deadline = Date.now() + 5000;
|
|
177
|
+
while (Date.now() < deadline) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(Number(pid), 0);
|
|
180
|
+
} catch {
|
|
181
|
+
return pid; // gone
|
|
182
|
+
}
|
|
183
|
+
execFileSync("/bin/sleep", ["0.2"]);
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
process.kill(Number(pid), "SIGKILL");
|
|
187
|
+
} catch {
|
|
188
|
+
// Raced us to exit.
|
|
189
|
+
}
|
|
190
|
+
return pid;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
114
196
|
/**
|
|
115
197
|
* @param binPath the extracted server binary to install
|
|
116
198
|
* @param version the wrapper's version, shown in Finder's Get Info
|
|
199
|
+
* @returns {{ app: string, stoppedPid: string | null }}
|
|
117
200
|
*/
|
|
118
201
|
function installMacApp(binPath, version) {
|
|
119
202
|
if (process.platform !== "darwin") {
|
|
@@ -123,6 +206,10 @@ function installMacApp(binPath, version) {
|
|
|
123
206
|
);
|
|
124
207
|
}
|
|
125
208
|
|
|
209
|
+
// Before anything is replaced: a running copy would survive the swap and keep serving the
|
|
210
|
+
// version it started with.
|
|
211
|
+
const stoppedPid = stopRunningApp();
|
|
212
|
+
|
|
126
213
|
const app = appDir();
|
|
127
214
|
const macos = path.join(app, "Contents", "MacOS");
|
|
128
215
|
const resources = path.join(app, "Contents", "Resources");
|
|
@@ -160,14 +247,16 @@ function installMacApp(binPath, version) {
|
|
|
160
247
|
// Cosmetic only — the app works either way.
|
|
161
248
|
}
|
|
162
249
|
|
|
163
|
-
return app;
|
|
250
|
+
return { app, stoppedPid };
|
|
164
251
|
}
|
|
165
252
|
|
|
166
253
|
function uninstallMacApp() {
|
|
254
|
+
// Removing the bundle from under a running server leaves it running with no way back to it.
|
|
255
|
+
const stoppedPid = stopRunningApp();
|
|
167
256
|
const app = appDir();
|
|
168
|
-
if (!fs.existsSync(app)) return null;
|
|
257
|
+
if (!fs.existsSync(app)) return { app: null, stoppedPid };
|
|
169
258
|
fs.rmSync(app, { recursive: true, force: true });
|
|
170
|
-
return app;
|
|
259
|
+
return { app, stoppedPid };
|
|
171
260
|
}
|
|
172
261
|
|
|
173
|
-
module.exports = { installMacApp, uninstallMacApp, appDir, APP_NAME };
|
|
262
|
+
module.exports = { installMacApp, uninstallMacApp, stopRunningApp, appDir, APP_NAME };
|