latex-stickies 1.4.0 → 1.4.1

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/README.md CHANGED
@@ -23,6 +23,11 @@ npm install -g latex-stickies
23
23
  latex-stickies
24
24
  ```
25
25
 
26
+ If a launch fails, the launcher says so and shows the runtime's own output;
27
+ the full log is kept at `~/Library/Logs/latex-stickies/launch.log` on macOS,
28
+ `%LOCALAPPDATA%\latex-stickies\` on Windows, and `$XDG_STATE_HOME` (or
29
+ `~/.local/state/latex-stickies/`) on Linux.
30
+
26
31
  Requires Node 22.12 or newer. The first run downloads the Electron runtime
27
32
  (~230 MB), so give it a minute; later launches are instant. On a Mac it also
28
33
  keeps a copy of the runtime carrying this app's name and icon in
@@ -6,10 +6,26 @@
6
6
  * We spawn it against this package's own directory, detached, so the notes
7
7
  * keep running after the terminal that started them is closed -- a sticky
8
8
  * note that dies with your shell is not a sticky note.
9
+ *
10
+ * Detached, but not unwatched. This used to print "LaTeX Stickies is running"
11
+ * the instant it had spawned something, with output going to /dev/null, so a
12
+ * runtime that started and died a moment later -- a missing shared library, a
13
+ * half-downloaded Electron, root without --no-sandbox -- reported success and
14
+ * left nothing behind to read. The launcher now keeps the child for a couple
15
+ * of seconds and says what happened if it does not survive.
9
16
  */
10
17
  const { spawn } = require('child_process');
18
+ const fs = require('fs');
19
+ const os = require('os');
11
20
  const path = require('path');
12
21
 
22
+ /** How long a launch has to survive before we call it a launch. */
23
+ const STARTUP_MS = 2000;
24
+ /** How much of the log to show when it does not. */
25
+ const LOG_TAIL_LINES = 20;
26
+ /** Kept from growing without bound: each launch starts a fresh log. */
27
+ const LOG_NAME = 'launch.log';
28
+
13
29
  // Brand before resolving anything: this returns the path to a copy of the
14
30
  // Electron shell carrying our name and icon, kept outside node_modules so it
15
31
  // survives npx cache churn and npm ci. Falls back to the plain shell, which
@@ -37,16 +53,108 @@ if (!electron) {
37
53
  }
38
54
  }
39
55
 
56
+ /** Where this platform expects a log to live. */
57
+ function logDirectory() {
58
+ if (process.platform === 'darwin') {
59
+ return path.join(os.homedir(), 'Library', 'Logs', 'latex-stickies');
60
+ }
61
+ if (process.platform === 'win32') {
62
+ const base = process.env.LOCALAPPDATA
63
+ || path.join(os.homedir(), 'AppData', 'Local');
64
+ return path.join(base, 'latex-stickies');
65
+ }
66
+ const base = process.env.XDG_STATE_HOME
67
+ || path.join(os.homedir(), '.local', 'state');
68
+ return path.join(base, 'latex-stickies');
69
+ }
70
+
71
+ /**
72
+ * Opens the log the app's output goes to.
73
+ *
74
+ * Truncated on every launch rather than appended to: it exists to explain the
75
+ * launch that just failed, and that keeps it bounded without any rotation.
76
+ * If it cannot be opened -- a read-only home, an odd container -- the launch
77
+ * still goes ahead with the output discarded, because a missing log is a far
78
+ * smaller problem than refusing to start.
79
+ */
80
+ function openLog() {
81
+ try {
82
+ const dir = logDirectory();
83
+ fs.mkdirSync(dir, { recursive: true });
84
+ const file = path.join(dir, LOG_NAME);
85
+ return { file, fd: fs.openSync(file, 'w') };
86
+ } catch (_) {
87
+ return { file: null, fd: 'ignore' };
88
+ }
89
+ }
90
+
91
+ /** The tail of the log, for a failure message. */
92
+ function logTail(file) {
93
+ if (!file) return '';
94
+ try {
95
+ const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
96
+ return lines.slice(-LOG_TAIL_LINES).join('\n');
97
+ } catch (_) {
98
+ return '';
99
+ }
100
+ }
101
+
102
+ const { file: logFile, fd: logFd } = openLog();
103
+
40
104
  const appDir = path.join(__dirname, '..');
41
105
  const child = spawn(electron, [appDir, ...process.argv.slice(2)], {
42
106
  detached: true,
43
- stdio: 'ignore',
107
+ stdio: ['ignore', logFd, logFd],
44
108
  });
45
109
 
110
+ // The child has its own handle on the log now; this one would otherwise keep
111
+ // the descriptor open for the life of the parent.
112
+ if (typeof logFd === 'number') {
113
+ try { fs.closeSync(logFd); } catch (_) { /* already gone */ }
114
+ }
115
+
46
116
  child.on('error', (err) => {
47
- console.error('Failed to launch LaTeX Stickies:', err.message);
117
+ console.error(`Failed to launch LaTeX Stickies: ${err.message}`);
48
118
  process.exit(1);
49
119
  });
50
120
 
51
- child.unref();
52
- console.log('LaTeX Stickies is running. Close the notes to quit.');
121
+ /**
122
+ * Electron will not run as root without --no-sandbox, and says so only in the
123
+ * output nobody was reading. Naming it beats adding the flag quietly: turning
124
+ * off the sandbox is a decision for whoever is running as root to make.
125
+ */
126
+ function rootHint() {
127
+ if (process.getuid && process.getuid() === 0) {
128
+ return '\nRunning as root: Electron refuses to start without a sandbox.'
129
+ + '\nRun it as an ordinary user, or pass --no-sandbox if you mean it:'
130
+ + '\n latex-stickies --no-sandbox';
131
+ }
132
+ return '';
133
+ }
134
+
135
+ const onEarlyExit = (code, signal) => {
136
+ clearTimeout(timer);
137
+ const tail = logTail(logFile);
138
+ console.error(
139
+ `LaTeX Stickies exited immediately (code=${code}, signal=${signal}).`
140
+ + rootHint()
141
+ + (tail ? `\n\nLast ${LOG_TAIL_LINES} lines of the log:\n${tail}` : '')
142
+ + (logFile ? `\n\nFull log: ${logFile}` : '\n\nNo log could be written.')
143
+ );
144
+ process.exit(1);
145
+ };
146
+
147
+ child.on('exit', onEarlyExit);
148
+
149
+ // Survived the window: let go of it and let this process end, so the terminal
150
+ // is not held open by a note that is running perfectly well.
151
+ const timer = setTimeout(() => {
152
+ child.removeListener('exit', onEarlyExit);
153
+ child.removeAllListeners('error');
154
+ child.unref();
155
+ console.log('LaTeX Stickies is running. Close the notes to quit.');
156
+ }, STARTUP_MS);
157
+
158
+ // Watching must not become waiting: an unreferenced timer still fires, but it
159
+ // no longer holds the event loop open on its own.
160
+ timer.unref();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latex-stickies",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Sticky notes for your desktop that render LaTeX and Markdown.",
5
5
  "main": "src/main.js",
6
6
  "scripts": {
@@ -63,6 +63,12 @@ function source() {
63
63
  const binary = require(path.join(dir, 'index.js'));
64
64
  // .../Electron.app/Contents/MacOS/Electron -> .../Electron.app
65
65
  const app = path.resolve(path.dirname(binary), '..', '..');
66
+ // Make sure that really is a bundle before anything is built from it. A
67
+ // stub or an odd install resolves to something that is not an .app, and the
68
+ // build then fails partway and takes the existing branded copy with it.
69
+ if (!app.endsWith('.app') || !fs.existsSync(path.join(app, 'Contents', 'MacOS'))) {
70
+ throw new Error(`the Electron runtime at ${app} is not an app bundle`);
71
+ }
66
72
  const { version } = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
67
73
  return { app, version };
68
74
  }
@@ -173,8 +179,12 @@ function ensure() {
173
179
  return binary;
174
180
  } catch (err) {
175
181
  // Leave nothing half-built: a bundle with a broken signature will not
176
- // launch at all, which is far worse than the wrong name.
177
- fs.rmSync(TARGET, { recursive: true, force: true });
182
+ // launch at all, which is far worse than the wrong name. Only what this
183
+ // run was building, though -- a failure part way through is no reason to
184
+ // remove a copy that was working before it.
185
+ if (!isCurrent(version)) {
186
+ fs.rmSync(TARGET, { recursive: true, force: true });
187
+ }
178
188
  report(`could not brand the app (${first(err)})`);
179
189
  return null;
180
190
  }