@bobfrankston/msger 0.1.351 → 0.1.355
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/bin/msgernative.exe +0 -0
- package/msger-native/builder/postinstall.js +16 -0
- package/package.json +1 -1
- package/shower.js +120 -42
|
Binary file
|
|
@@ -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
package/shower.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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';
|
|
@@ -113,61 +113,139 @@ function findMsgerSource() {
|
|
|
113
113
|
return bundled;
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
/** Sweep stale `.aside-*` / `.old-*` siblings of `perAppExe`. Best-effort —
|
|
117
|
+
* files held open by an older instance throw EPERM and stay until the next
|
|
118
|
+
* launch when nothing has them mapped. */
|
|
119
|
+
function sweepAsideSiblings(perAppExe) {
|
|
120
|
+
try {
|
|
121
|
+
const dir = path.dirname(perAppExe);
|
|
122
|
+
const base = path.basename(perAppExe);
|
|
123
|
+
const pat = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.(?:old|aside|tmp)-(\\d+)$`);
|
|
124
|
+
for (const f of fs.readdirSync(dir)) {
|
|
125
|
+
const m = pat.exec(f);
|
|
126
|
+
if (!m)
|
|
127
|
+
continue;
|
|
128
|
+
try {
|
|
129
|
+
fs.unlinkSync(path.join(dir, f));
|
|
130
|
+
}
|
|
131
|
+
catch { /* still locked, try next time */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch { /* fall through */ }
|
|
135
|
+
}
|
|
136
|
+
/** Inject `.ico` into `exePath` as the IDI_ICON1 / RT_GROUP_ICON resource so
|
|
137
|
+
* the per-app exe shows the app's own icon in Explorer, Task Manager, the
|
|
138
|
+
* taskbar, and pinned shortcuts — every Windows context that reads the exe
|
|
139
|
+
* resource (which `with_window_icon` doesn't reach). Sync — invokes the
|
|
140
|
+
* shipped `msgernative.exe --update-icon` subcommand (no external deps;
|
|
141
|
+
* msger stays self-contained). Returns true on success. Failures don't
|
|
142
|
+
* break launch — the launcher just runs without the embedded icon. */
|
|
143
|
+
function injectIcon(msgerNative, exePath, icoPath) {
|
|
144
|
+
try {
|
|
145
|
+
execFileSync(msgerNative, ["--update-icon", exePath, icoPath], { stdio: "pipe" });
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
const stderr = (e?.stderr ?? "").toString().trim();
|
|
150
|
+
console.error(` msger: --update-icon failed for ${exePath}: ${e?.message || e}${stderr ? ` (${stderr})` : ""}`);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Ensure a per-app exe is present at `perAppExe`, derived from `sourceExe`,
|
|
155
|
+
* with the per-app icon resource injected. Two modes:
|
|
156
|
+
*
|
|
157
|
+
* - `_appIcon` is set (the per-app-identity case): the per-app exe is a
|
|
158
|
+
* *real copy*, not a hardlink, because `--update-icon` rewrites the resource
|
|
159
|
+
* section and we don't want that change to bleed back into the shared
|
|
160
|
+
* `msgernative.exe` (which a hardlink would). Staleness compared via
|
|
161
|
+
* mtime of source exe + mtime of source icon. Refresh uses the
|
|
162
|
+
* rename-aside pattern so a running instance doesn't block the update.
|
|
163
|
+
*
|
|
164
|
+
* - `_appIcon` is unset: legacy hardlink behavior — `mailx.exe` shares an
|
|
165
|
+
* inode with `msgernative.exe`, no resource injection. Same as before.
|
|
166
|
+
*/
|
|
122
167
|
function ensureFresh(perAppExe, sourceExe) {
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
fs.mkdirSync(path.dirname(perAppExe), { recursive: true });
|
|
169
|
+
// Hardlink path (callers that didn't `setAppIcon`) — unchanged behavior.
|
|
170
|
+
if (!_appIcon || !fs.existsSync(_appIcon)) {
|
|
171
|
+
if (!fs.existsSync(perAppExe)) {
|
|
172
|
+
try {
|
|
173
|
+
fs.linkSync(sourceExe, perAppExe);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
fs.copyFileSync(sourceExe, perAppExe);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (fs.statSync(perAppExe).ino === fs.statSync(sourceExe).ino)
|
|
181
|
+
return;
|
|
182
|
+
const aside = `${perAppExe}.aside-${Date.now()}`;
|
|
183
|
+
try {
|
|
184
|
+
fs.renameSync(perAppExe, aside);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
125
189
|
try {
|
|
126
190
|
fs.linkSync(sourceExe, perAppExe);
|
|
127
191
|
}
|
|
128
192
|
catch {
|
|
129
|
-
fs.copyFileSync(sourceExe, perAppExe);
|
|
193
|
+
fs.copyFileSync(sourceExe, perAppExe);
|
|
130
194
|
}
|
|
195
|
+
sweepAsideSiblings(perAppExe);
|
|
131
196
|
return;
|
|
132
197
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return; /* held open by another instance — retry next launch */
|
|
198
|
+
// Copy + --update-icon path. Stale when the per-app exe is missing OR the source
|
|
199
|
+
// exe is newer (msger updated) OR the source icon is newer (icon changed).
|
|
200
|
+
const srcMtime = fs.statSync(sourceExe).mtimeMs;
|
|
201
|
+
const icoMtime = fs.statSync(_appIcon).mtimeMs;
|
|
202
|
+
let stale = !fs.existsSync(perAppExe);
|
|
203
|
+
if (!stale) {
|
|
204
|
+
const dstMtime = fs.statSync(perAppExe).mtimeMs;
|
|
205
|
+
stale = dstMtime < srcMtime || dstMtime < icoMtime;
|
|
144
206
|
}
|
|
207
|
+
if (!stale)
|
|
208
|
+
return;
|
|
209
|
+
// Build the new exe out-of-band so a running instance never sees a
|
|
210
|
+
// half---update-icon'd file. Sequence: copy → --update-icon tmp → rename old aside →
|
|
211
|
+
// rename tmp into place. All renames work even on locked files; only
|
|
212
|
+
// delete is blocked, and we defer that to the next launch's sweep.
|
|
213
|
+
const stamp = Date.now();
|
|
214
|
+
const tmpExe = `${perAppExe}.tmp-${stamp}`;
|
|
215
|
+
const isIco = _appIcon.toLowerCase().endsWith(".ico");
|
|
145
216
|
try {
|
|
146
|
-
fs.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
158
|
-
for (const f of fs.readdirSync(dir)) {
|
|
159
|
-
const m = oldPat.exec(f);
|
|
160
|
-
if (!m)
|
|
161
|
-
continue;
|
|
162
|
-
if (Number(m[1]) > cutoff)
|
|
163
|
-
continue;
|
|
217
|
+
fs.copyFileSync(sourceExe, tmpExe);
|
|
218
|
+
let injected = false;
|
|
219
|
+
if (isIco) {
|
|
220
|
+
injected = injectIcon(sourceExe, tmpExe, _appIcon);
|
|
221
|
+
}
|
|
222
|
+
if (!isIco || !injected) {
|
|
223
|
+
// No .ico available, or the resource update failed — fall back
|
|
224
|
+
// to a plain copy. Window-icon path still gets set via Tao at
|
|
225
|
+
// launch; we just don't get the EXE-level resource.
|
|
226
|
+
}
|
|
227
|
+
if (fs.existsSync(perAppExe)) {
|
|
164
228
|
try {
|
|
165
|
-
fs.
|
|
229
|
+
fs.renameSync(perAppExe, `${perAppExe}.aside-${stamp}`);
|
|
230
|
+
}
|
|
231
|
+
catch { /* held open — leave the old file in place; we'll retry */
|
|
232
|
+
try {
|
|
233
|
+
fs.unlinkSync(tmpExe);
|
|
234
|
+
}
|
|
235
|
+
catch { /* */ }
|
|
236
|
+
return;
|
|
166
237
|
}
|
|
167
|
-
catch { /* still locked, try next time */ }
|
|
168
238
|
}
|
|
239
|
+
fs.renameSync(tmpExe, perAppExe);
|
|
240
|
+
sweepAsideSiblings(perAppExe);
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
console.error(` msger: per-app exe provision failed (${perAppExe}): ${e?.message || e}`);
|
|
244
|
+
try {
|
|
245
|
+
fs.unlinkSync(tmpExe);
|
|
246
|
+
}
|
|
247
|
+
catch { /* */ }
|
|
169
248
|
}
|
|
170
|
-
catch { /* fall through */ }
|
|
171
249
|
}
|
|
172
250
|
/** Copy the per-app icon next to the per-app exe so Rust's default-icon
|
|
173
251
|
* search (`load_icon` in main.rs) picks it up. Rust looks for `msger.ico`
|