@bobfrankston/msger 0.1.351 → 0.1.353
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/msger-native/builder/postinstall.js +16 -0
- package/package.json +3 -2
- package/shower.js +156 -42
|
@@ -197,3 +197,19 @@ if (!isWindows) {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
+
|
|
201
|
+
// Friendly message about the EPERM warnings npm itself emits during a
|
|
202
|
+
// subsequent global upgrade. They come from npm trying to delete its own
|
|
203
|
+
// staging directory while the previously-running msgernative.exe inside
|
|
204
|
+
// still holds a file handle. msger's rename-aside scheme above has already
|
|
205
|
+
// handled the actual upgrade safely; the warnings are cosmetic.
|
|
206
|
+
if (isWindows) {
|
|
207
|
+
console.log("");
|
|
208
|
+
console.log(" msger: install OK.");
|
|
209
|
+
console.log(" msger: if a future `npm i -g @bobfrankston/msger` prints");
|
|
210
|
+
console.log(" EPERM/ENOTEMPTY warnings about removing a staging");
|
|
211
|
+
console.log(" directory, those are expected when an older msger");
|
|
212
|
+
console.log(" is still running — the upgrade has already taken");
|
|
213
|
+
console.log(" effect via rename-aside. Safe to ignore.");
|
|
214
|
+
console.log("");
|
|
215
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/msger",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.353",
|
|
4
4
|
"description": "Fast, lightweight, cross-platform message box - Rust-powered alternative to msgview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@bobfrankston/msgcommon": "^0.1.23",
|
|
53
53
|
"ansi-to-html": "^0.7.2",
|
|
54
|
-
"json5": "^2.2.3"
|
|
54
|
+
"json5": "^2.2.3",
|
|
55
|
+
"rcedit": "^4.0.1"
|
|
55
56
|
},
|
|
56
57
|
"files": [
|
|
57
58
|
"*.js",
|
package/shower.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
1
|
+
import { spawn, execFileSync } from 'child_process';
|
|
2
2
|
import { platform } from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
|
+
import { createRequire } from 'module';
|
|
5
6
|
import fs from 'fs';
|
|
6
7
|
import AnsiToHtml from 'ansi-to-html';
|
|
7
8
|
// Load package.json for version
|
|
@@ -113,61 +114,174 @@ function findMsgerSource() {
|
|
|
113
114
|
return bundled;
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
/** Sweep stale `.aside-*` / `.old-*` siblings of `perAppExe`. Best-effort —
|
|
118
|
+
* files held open by an older instance throw EPERM and stay until the next
|
|
119
|
+
* launch when nothing has them mapped. */
|
|
120
|
+
function sweepAsideSiblings(perAppExe) {
|
|
121
|
+
try {
|
|
122
|
+
const dir = path.dirname(perAppExe);
|
|
123
|
+
const base = path.basename(perAppExe);
|
|
124
|
+
const pat = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.(?:old|aside|tmp)-(\\d+)$`);
|
|
125
|
+
for (const f of fs.readdirSync(dir)) {
|
|
126
|
+
const m = pat.exec(f);
|
|
127
|
+
if (!m)
|
|
128
|
+
continue;
|
|
129
|
+
try {
|
|
130
|
+
fs.unlinkSync(path.join(dir, f));
|
|
131
|
+
}
|
|
132
|
+
catch { /* still locked, try next time */ }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch { /* fall through */ }
|
|
136
|
+
}
|
|
137
|
+
/** Locate the rcedit-<arch>.exe shipped with the `rcedit` npm package
|
|
138
|
+
* (Electron-team maintained, published unscoped). Returns null on
|
|
139
|
+
* non-Windows or if the package isn't installed. We resolve the path
|
|
140
|
+
* manually so we can call the binary with `execFileSync` and keep the
|
|
141
|
+
* caller synchronous. */
|
|
142
|
+
function findRceditBinary() {
|
|
143
|
+
if (platform() !== "win32")
|
|
144
|
+
return null;
|
|
145
|
+
try {
|
|
146
|
+
// The package ships rcedit-x64.exe / rcedit-ia32.exe / rcedit-arm64.exe
|
|
147
|
+
// in its `bin/` dir. Resolve via require.resolve('rcedit') → walk up
|
|
148
|
+
// to the package root, then into bin/.
|
|
149
|
+
const pkgEntry = createRequire(import.meta.url).resolve("rcedit");
|
|
150
|
+
const pkgRoot = path.dirname(pkgEntry);
|
|
151
|
+
const archMap = {
|
|
152
|
+
x64: "rcedit-x64.exe",
|
|
153
|
+
ia32: "rcedit-ia32.exe",
|
|
154
|
+
arm64: "rcedit-arm64.exe",
|
|
155
|
+
};
|
|
156
|
+
const exeName = archMap[process.arch] || "rcedit-x64.exe";
|
|
157
|
+
const candidates = [
|
|
158
|
+
path.join(pkgRoot, "bin", exeName),
|
|
159
|
+
path.join(pkgRoot, "..", "bin", exeName),
|
|
160
|
+
];
|
|
161
|
+
for (const c of candidates) {
|
|
162
|
+
if (fs.existsSync(c))
|
|
163
|
+
return c;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch { /* not installed — caller will fall back to plain copy */ }
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
/** Inject `.ico` into `exePath` as the IDI_ICON1 / RT_GROUP_ICON resource so
|
|
170
|
+
* the per-app exe shows the app's own icon in Explorer, Task Manager, the
|
|
171
|
+
* taskbar, and pinned shortcuts — every Windows context that reads the exe
|
|
172
|
+
* resource (which `with_window_icon` doesn't reach). Sync — fires the
|
|
173
|
+
* bundled rcedit.exe via `execFileSync`. Returns true on success. Failures
|
|
174
|
+
* don't break launch — the launcher just runs without the embedded icon. */
|
|
175
|
+
function injectIcon(exePath, icoPath) {
|
|
176
|
+
const rcedit = findRceditBinary();
|
|
177
|
+
if (!rcedit) {
|
|
178
|
+
console.error(` msger: rcedit binary not found — per-app icon not injected (install \`rcedit\` npm package)`);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
execFileSync(rcedit, [exePath, "--set-icon", icoPath], { stdio: "pipe" });
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
console.error(` msger: rcedit failed for ${exePath}: ${e?.message || e}`);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Ensure a per-app exe is present at `perAppExe`, derived from `sourceExe`,
|
|
191
|
+
* with the per-app icon resource injected. Two modes:
|
|
192
|
+
*
|
|
193
|
+
* - `_appIcon` is set (the per-app-identity case): the per-app exe is a
|
|
194
|
+
* *real copy*, not a hardlink, because `rcedit` rewrites the resource
|
|
195
|
+
* section and we don't want that change to bleed back into the shared
|
|
196
|
+
* `msgernative.exe` (which a hardlink would). Staleness compared via
|
|
197
|
+
* mtime of source exe + mtime of source icon. Refresh uses the
|
|
198
|
+
* rename-aside pattern so a running instance doesn't block the update.
|
|
199
|
+
*
|
|
200
|
+
* - `_appIcon` is unset: legacy hardlink behavior — `mailx.exe` shares an
|
|
201
|
+
* inode with `msgernative.exe`, no resource injection. Same as before.
|
|
202
|
+
*/
|
|
122
203
|
function ensureFresh(perAppExe, sourceExe) {
|
|
123
|
-
|
|
124
|
-
|
|
204
|
+
fs.mkdirSync(path.dirname(perAppExe), { recursive: true });
|
|
205
|
+
// Hardlink path (callers that didn't `setAppIcon`) — unchanged behavior.
|
|
206
|
+
if (!_appIcon || !fs.existsSync(_appIcon)) {
|
|
207
|
+
if (!fs.existsSync(perAppExe)) {
|
|
208
|
+
try {
|
|
209
|
+
fs.linkSync(sourceExe, perAppExe);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
fs.copyFileSync(sourceExe, perAppExe);
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (fs.statSync(perAppExe).ino === fs.statSync(sourceExe).ino)
|
|
217
|
+
return;
|
|
218
|
+
const aside = `${perAppExe}.aside-${Date.now()}`;
|
|
219
|
+
try {
|
|
220
|
+
fs.renameSync(perAppExe, aside);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
125
225
|
try {
|
|
126
226
|
fs.linkSync(sourceExe, perAppExe);
|
|
127
227
|
}
|
|
128
228
|
catch {
|
|
129
|
-
fs.copyFileSync(sourceExe, perAppExe);
|
|
229
|
+
fs.copyFileSync(sourceExe, perAppExe);
|
|
130
230
|
}
|
|
231
|
+
sweepAsideSiblings(perAppExe);
|
|
131
232
|
return;
|
|
132
233
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
234
|
+
// Copy + rcedit path. Stale when the per-app exe is missing OR the source
|
|
235
|
+
// exe is newer (msger updated) OR the source icon is newer (icon changed).
|
|
236
|
+
const srcMtime = fs.statSync(sourceExe).mtimeMs;
|
|
237
|
+
const icoMtime = fs.statSync(_appIcon).mtimeMs;
|
|
238
|
+
let stale = !fs.existsSync(perAppExe);
|
|
239
|
+
if (!stale) {
|
|
240
|
+
const dstMtime = fs.statSync(perAppExe).mtimeMs;
|
|
241
|
+
stale = dstMtime < srcMtime || dstMtime < icoMtime;
|
|
141
242
|
}
|
|
142
|
-
|
|
143
|
-
return;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Best-effort sweep of `.old-*` siblings older than 24 h. Same pattern as
|
|
152
|
-
// postinstall.js. Skip silently — they'll get cleaned up next time.
|
|
243
|
+
if (!stale)
|
|
244
|
+
return;
|
|
245
|
+
// Build the new exe out-of-band so a running instance never sees a
|
|
246
|
+
// half-rcedit'd file. Sequence: copy → rcedit tmp → rename old aside →
|
|
247
|
+
// rename tmp into place. All renames work even on locked files; only
|
|
248
|
+
// delete is blocked, and we defer that to the next launch's sweep.
|
|
249
|
+
const stamp = Date.now();
|
|
250
|
+
const tmpExe = `${perAppExe}.tmp-${stamp}`;
|
|
251
|
+
const isIco = _appIcon.toLowerCase().endsWith(".ico");
|
|
153
252
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
253
|
+
fs.copyFileSync(sourceExe, tmpExe);
|
|
254
|
+
let rceditOk = false;
|
|
255
|
+
if (isIco) {
|
|
256
|
+
rceditOk = injectIcon(tmpExe, _appIcon);
|
|
257
|
+
}
|
|
258
|
+
if (!isIco || !rceditOk) {
|
|
259
|
+
// No .ico available, or rcedit couldn't inject — fall back to a
|
|
260
|
+
// plain copy. Window-icon path still gets set via Tao at launch;
|
|
261
|
+
// we just don't get the EXE-level resource.
|
|
262
|
+
}
|
|
263
|
+
if (fs.existsSync(perAppExe)) {
|
|
164
264
|
try {
|
|
165
|
-
fs.
|
|
265
|
+
fs.renameSync(perAppExe, `${perAppExe}.aside-${stamp}`);
|
|
266
|
+
}
|
|
267
|
+
catch { /* held open — leave the old file in place; we'll retry */
|
|
268
|
+
try {
|
|
269
|
+
fs.unlinkSync(tmpExe);
|
|
270
|
+
}
|
|
271
|
+
catch { /* */ }
|
|
272
|
+
return;
|
|
166
273
|
}
|
|
167
|
-
catch { /* still locked, try next time */ }
|
|
168
274
|
}
|
|
275
|
+
fs.renameSync(tmpExe, perAppExe);
|
|
276
|
+
sweepAsideSiblings(perAppExe);
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
console.error(` msger: per-app exe provision failed (${perAppExe}): ${e?.message || e}`);
|
|
280
|
+
try {
|
|
281
|
+
fs.unlinkSync(tmpExe);
|
|
282
|
+
}
|
|
283
|
+
catch { /* */ }
|
|
169
284
|
}
|
|
170
|
-
catch { /* fall through */ }
|
|
171
285
|
}
|
|
172
286
|
/** Copy the per-app icon next to the per-app exe so Rust's default-icon
|
|
173
287
|
* search (`load_icon` in main.rs) picks it up. Rust looks for `msger.ico`
|