@bobfrankston/msger 0.1.328 → 0.1.330

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.
Binary file
@@ -65,7 +65,16 @@ if (fs.existsSync(buildBinary)) {
65
65
  }
66
66
 
67
67
  // Step 2: Create timestamped copy in the user bin dir (NOT node_modules)
68
- try { fs.mkdirSync(userBinDir, { recursive: true }); } catch { /* exists */ }
68
+ try {
69
+ fs.mkdirSync(userBinDir, { recursive: true });
70
+ } catch (e) {
71
+ // recursive:true normally makes this idempotent, so a real failure here
72
+ // means the path can't be created at all (permissions, disk full, etc.).
73
+ if (e.code !== "EEXIST") {
74
+ console.error(` msger: could not create user bin dir ${userBinDir}: ${e.message}`);
75
+ throw e;
76
+ }
77
+ }
69
78
  const stamp = Date.now();
70
79
  const versionedBinary = path.join(userBinDir, `${baseName}-${stamp}${ext}`);
71
80
  if (fs.existsSync(srcBinary)) {
@@ -78,44 +87,113 @@ if (fs.existsSync(srcBinary)) {
78
87
 
79
88
  // Step 2b: Create/update a stable-name hardlink so the launcher always uses
80
89
  // the same path. Taskbar pins and AUMID grouping depend on a fixed exe path.
81
- // Hardlink shares inode with the timestamped copy — deleting the old link
82
- // doesn't affect a running process (it holds its own file handle).
90
+ //
91
+ // Rename-aside pattern: Windows lets you rename a running exe's path but
92
+ // refuses to delete it. So we rename the current stable link aside (to a
93
+ // timestamped `.old-<ts>` name) before creating the new hardlink. This works
94
+ // whether or not the old binary is currently executing — the retired name
95
+ // keeps the running process's path valid; the stable slot is freed for the
96
+ // new hardlink. Previously we tried unlink+link, which silently failed when
97
+ // the stable name was locked, leaving users on a stale binary indefinitely.
83
98
  const stableLink = path.join(userBinDir, `${baseName}${ext}`);
99
+ if (fs.existsSync(stableLink)) {
100
+ const retired = path.join(userBinDir, `${baseName}${ext}.old-${stamp}`);
101
+ try {
102
+ fs.renameSync(stableLink, retired);
103
+ } catch (e) {
104
+ console.error(` msger: could not rename stable link aside: ${e.message}`);
105
+ console.error(` msger: update cannot proceed — stable link ${stableLink} is in an unexpected state`);
106
+ }
107
+ }
84
108
  try {
85
- // Remove old link (may point to previous version)
86
- try { fs.unlinkSync(stableLink); } catch { /* didn't exist */ }
87
109
  fs.linkSync(versionedBinary, stableLink);
88
110
  } catch (e) {
89
- // Hardlink failed (cross-device, locked, etc.) copy as fallback
90
- try { fs.copyFileSync(versionedBinary, stableLink); } catch { /* */ }
111
+ // Hardlink failed (cross-device, weird FS). Copy as a fallback.
112
+ console.error(` msger: hardlink ${stableLink} -> ${versionedBinary} failed (${e.message}); falling back to copy`);
113
+ try {
114
+ fs.copyFileSync(versionedBinary, stableLink);
115
+ } catch (e2) {
116
+ console.error(` msger: copy fallback also failed: ${e2.message}`);
117
+ console.error(` msger: launcher will fall back to timestamped copies; taskbar pins may not group.`);
118
+ }
119
+ }
120
+
121
+ // Step 2c: Clean up retired `.old-<ts>` copies from previous updates. A file
122
+ // that's still locked by a not-yet-exited process stays behind and gets
123
+ // retried on the NEXT update — that's expected, so an EBUSY/EPERM on unlink
124
+ // isn't an error, but anything else is.
125
+ const oldPattern = new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}${ext.replace('.', '\\.')}\\.old-\\d+$`);
126
+ try {
127
+ for (const f of fs.readdirSync(userBinDir)) {
128
+ if (!oldPattern.test(f)) continue;
129
+ const full = path.join(userBinDir, f);
130
+ try {
131
+ fs.unlinkSync(full);
132
+ } catch (e) {
133
+ if (e.code !== "EBUSY" && e.code !== "EPERM") {
134
+ console.error(` msger: unexpected error cleaning up ${f}: ${e.message}`);
135
+ }
136
+ // else: expected — owning process still running, defer to next update
137
+ }
138
+ }
139
+ } catch (e) {
140
+ console.error(` msger: could not scan ${userBinDir} for retired copies: ${e.message}`);
91
141
  }
92
142
 
93
143
  // Step 3a: Clean up old timestamped copies in the user bin dir (keep the one
94
- // we just created). Locked copies are silently skipped they get cleaned up
95
- // on the next install once their owning process has exited.
144
+ // we just created). EBUSY/EPERM on a locked copy means the owning process is
145
+ // still running expected, retry on next update. Anything else is unexpected
146
+ // and gets logged.
96
147
  const pattern = new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-(\\d+)${ext.replace('.', '\\.')}$`);
97
148
  try {
98
149
  for (const f of fs.readdirSync(userBinDir)) {
99
- if (f.match(pattern) && f !== path.basename(versionedBinary)) {
100
- try { fs.unlinkSync(path.join(userBinDir, f)); } catch { /* locked */ }
150
+ if (!f.match(pattern) || f === path.basename(versionedBinary)) continue;
151
+ const full = path.join(userBinDir, f);
152
+ try {
153
+ fs.unlinkSync(full);
154
+ } catch (e) {
155
+ if (e.code !== "EBUSY" && e.code !== "EPERM") {
156
+ console.error(` msger: unexpected error cleaning up ${f}: ${e.message}`);
157
+ }
101
158
  }
102
159
  }
103
- } catch { /* user dir didn't exist or unreadable */ }
160
+ } catch (e) {
161
+ console.error(` msger: could not scan ${userBinDir} for stale timestamped copies: ${e.message}`);
162
+ }
104
163
 
105
164
  // Step 3b: Migrate-and-clean any stale timestamped copies still living inside
106
165
  // node_modules from older installs. They serve no purpose now and may be the
107
166
  // EPERM blocker on upgrade.
108
167
  try {
109
168
  for (const f of fs.readdirSync(binDir)) {
110
- if (f.match(pattern)) {
111
- try { fs.unlinkSync(path.join(binDir, f)); } catch { /* locked */ }
169
+ if (!f.match(pattern)) continue;
170
+ const full = path.join(binDir, f);
171
+ try {
172
+ fs.unlinkSync(full);
173
+ } catch (e) {
174
+ if (e.code !== "EBUSY" && e.code !== "EPERM") {
175
+ console.error(` msger: unexpected error cleaning up legacy ${f}: ${e.message}`);
176
+ }
112
177
  }
113
178
  }
114
- } catch { /* ignore */ }
179
+ } catch (e) {
180
+ // binDir should always exist inside the package — if it doesn't, something
181
+ // is very wrong, but we can still let the install complete.
182
+ console.error(` msger: could not scan ${binDir} for legacy timestamped copies: ${e.message}`);
183
+ }
115
184
 
116
185
  // Step 4: Set permissions on Linux/Mac
117
186
  if (!isWindows) {
118
187
  for (const bin of [srcBinary, versionedBinary]) {
119
- try { fs.chmodSync(bin, 0o755); } catch { /* */ }
188
+ try {
189
+ fs.chmodSync(bin, 0o755);
190
+ } catch (e) {
191
+ // ENOENT = we never created that file (bin wasn't present); any
192
+ // other error (EACCES, EROFS, etc.) means the binary will fail to
193
+ // launch later, which is user-visible — log loud.
194
+ if (e.code !== "ENOENT") {
195
+ console.error(` msger: could not chmod ${bin}: ${e.message}`);
196
+ }
197
+ }
120
198
  }
121
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/msger",
3
- "version": "0.1.328",
3
+ "version": "0.1.330",
4
4
  "description": "Fast, lightweight, cross-platform message box - Rust-powered alternative to msgview",
5
5
  "type": "module",
6
6
  "main": "./index.js",