crucix 2.39.3 → 2.39.5
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 +17 -2
- package/apis/utils/rss-pool.mjs +6 -0
- package/lib/http-parser-error.mjs +10 -0
- package/lib/instance-lock.mjs +81 -0
- package/lib/release-binary.mjs +32 -0
- package/package.json +1 -1
- package/scripts/cli.mjs +6 -0
- package/scripts/install.ps1 +76 -0
- package/scripts/install.sh +80 -0
- package/server.mjs +16 -0
package/README.md
CHANGED
|
@@ -94,16 +94,31 @@ It was built for anyone who wants to understand what's actually happening in the
|
|
|
94
94
|
|
|
95
95
|
## Quick Start
|
|
96
96
|
|
|
97
|
+
```powershell
|
|
98
|
+
# Windows: standalone exe from GitHub Releases (no Node)
|
|
99
|
+
irm https://github.com/arlinamid/Crucix/releases/latest/download/install.ps1 | iex
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Linux / macOS
|
|
104
|
+
curl -fsSL https://github.com/arlinamid/Crucix/releases/latest/download/install.sh | sh
|
|
105
|
+
```
|
|
106
|
+
|
|
97
107
|
```bash
|
|
98
|
-
# From npm
|
|
108
|
+
# From npm: Node + runtime source, first run opens the setup wizard
|
|
99
109
|
npx crucix
|
|
100
110
|
npm install -g crucix
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Then:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
101
116
|
crucix --version
|
|
102
117
|
crucix install # ~/.crucix (.env and runs/)
|
|
103
118
|
crucix # dashboard; wizard if no .env yet
|
|
104
119
|
```
|
|
105
120
|
|
|
106
|
-
|
|
121
|
+
`npm run install:local` (from a git checkout) builds the exe yourself. Check which one PATH hits with `crucix --version`.
|
|
107
122
|
|
|
108
123
|
Settings the wizard and **Settings → News feeds** save go to `%USERPROFILE%\.crucix` on Windows or `~/.crucix` on Linux/macOS. Set `CRUCIX_HOME` to choose another folder, or `CRUCIX_ENV_FILE` to name the `.env` itself.
|
|
109
124
|
|
package/apis/utils/rss-pool.mjs
CHANGED
|
@@ -10,6 +10,12 @@ export const RSS_TIMEOUT_MS = 12000;
|
|
|
10
10
|
export const rssAgent = new Agent({
|
|
11
11
|
connections: RSS_CONCURRENCY,
|
|
12
12
|
pipelining: 0,
|
|
13
|
+
// Close the socket after each feed. Reusing a TLS connection that AbortSignal
|
|
14
|
+
// paused makes Node's HTTP parser throw assert(!this.paused) on socket end.
|
|
15
|
+
keepAliveTimeout: 1,
|
|
16
|
+
keepAliveMaxTimeout: 1,
|
|
17
|
+
bodyTimeout: RSS_TIMEOUT_MS,
|
|
18
|
+
headersTimeout: RSS_TIMEOUT_MS,
|
|
13
19
|
connect: {
|
|
14
20
|
timeout: 8000,
|
|
15
21
|
family: 4,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Node/undici can throw AssertionError `assert(!this.paused)` from Parser.finish when a
|
|
2
|
+
// TLS socket ends after AbortSignal paused the HTTP parser (RSS timeouts). The sweep
|
|
3
|
+
// already treated that feed as failed; the process must keep running.
|
|
4
|
+
|
|
5
|
+
export function isBenignHttpParserClose(error) {
|
|
6
|
+
if (!error || error.name !== 'AssertionError') return false;
|
|
7
|
+
const message = String(error.message || '');
|
|
8
|
+
const stack = String(error.stack || '');
|
|
9
|
+
return message.includes('assert(!this.paused)') && (stack.includes('Parser.finish') || stack.includes('onHttpSocketEnd'));
|
|
10
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// One long-running Crucix (dashboard or --collector) per runs/ profile.
|
|
2
|
+
// A second copy would share AIS, daily quotas and the same latest.json.
|
|
3
|
+
|
|
4
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const INSTANCE_LOCK_NAME = 'instance.lock';
|
|
8
|
+
|
|
9
|
+
export function instanceLockPath(runsDir) {
|
|
10
|
+
return join(runsDir, INSTANCE_LOCK_NAME);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function processIsAlive(pid) {
|
|
14
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return error?.code === 'EPERM';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function acquireInstanceLock(runsDir, { pid = process.pid, now = Date.now() } = {}) {
|
|
24
|
+
if (!runsDir) throw new TypeError('runsDir is required');
|
|
25
|
+
mkdirSync(runsDir, { recursive: true });
|
|
26
|
+
const path = instanceLockPath(runsDir);
|
|
27
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
const fd = openSync(path, 'wx');
|
|
30
|
+
try {
|
|
31
|
+
writeFileSync(fd, `${JSON.stringify({ pid, startedAt: new Date(now).toISOString() })}\n`);
|
|
32
|
+
} finally {
|
|
33
|
+
closeSync(fd);
|
|
34
|
+
}
|
|
35
|
+
return { path, pid, created: true };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
38
|
+
let other = null;
|
|
39
|
+
try { other = JSON.parse(readFileSync(path, 'utf8')); } catch { /* replace a corrupt lock */ }
|
|
40
|
+
if (Number(other?.pid) === pid) return { path, pid, created: false };
|
|
41
|
+
if (processIsAlive(Number(other?.pid))) {
|
|
42
|
+
return { error: 'already_running', pid: Number(other.pid), startedAt: other.startedAt || null, path };
|
|
43
|
+
}
|
|
44
|
+
try { unlinkSync(path); } catch { /* another process won the unlink */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { error: 'already_running', path };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function releaseInstanceLock(runsDir, { pid = process.pid } = {}) {
|
|
51
|
+
const path = instanceLockPath(runsDir);
|
|
52
|
+
if (!existsSync(path)) return false;
|
|
53
|
+
try {
|
|
54
|
+
const other = JSON.parse(readFileSync(path, 'utf8'));
|
|
55
|
+
if (Number(other?.pid) !== pid) return false;
|
|
56
|
+
unlinkSync(path);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function holdInstanceLock(runsDir, { exit = (code) => process.exit(code) } = {}) {
|
|
64
|
+
const lock = acquireInstanceLock(runsDir);
|
|
65
|
+
if (lock.error) return lock;
|
|
66
|
+
const release = () => { try { releaseInstanceLock(runsDir); } catch { /* process is leaving */ } };
|
|
67
|
+
process.on('exit', release);
|
|
68
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
69
|
+
process.on(signal, () => { release(); exit(0); });
|
|
70
|
+
}
|
|
71
|
+
return lock;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function alreadyRunningMessage(lock, { port, lang = 'en' } = {}) {
|
|
75
|
+
const pid = lock?.pid ? ` (pid ${lock.pid})` : '';
|
|
76
|
+
const where = port ? ` http://localhost:${port}` : '';
|
|
77
|
+
if (lang === 'hu') {
|
|
78
|
+
return `[Crucix] FATAL: már fut egy példány${pid}.${where ? ` Dashboard:${where}` : ''} Állítsd le azt, mielőtt újat indítasz.`;
|
|
79
|
+
}
|
|
80
|
+
return `[Crucix] FATAL: already running${pid}.${where ? ` Dashboard:${where}` : ''} Stop that instance before starting another.`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Names of the GitHub Release assets that `scripts/build-exe.mjs` and CI upload.
|
|
2
|
+
// install.ps1 / install.sh must stay in lockstep with this map.
|
|
3
|
+
|
|
4
|
+
const OS = Object.freeze({ win32: 'windows', linux: 'linux', darwin: 'macos' });
|
|
5
|
+
const ARCH = Object.freeze({ x64: 'x64', arm64: 'arm64' });
|
|
6
|
+
|
|
7
|
+
export function releaseAssetName(platform = process.platform, arch = process.arch) {
|
|
8
|
+
const os = OS[platform];
|
|
9
|
+
const cpu = ARCH[arch];
|
|
10
|
+
if (!os || !cpu) return null;
|
|
11
|
+
return `crucix-${os}-${cpu}${os === 'windows' ? '.exe' : ''}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function installedBinaryName(platform = process.platform) {
|
|
15
|
+
return platform === 'win32' ? 'crucix.exe' : 'crucix';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function checksumFor(sumsText, filename) {
|
|
19
|
+
if (!sumsText || !filename) return null;
|
|
20
|
+
for (const line of String(sumsText).split(/\r?\n/)) {
|
|
21
|
+
const match = line.match(/^([a-fA-F0-9]{64})\s+(\S+)\s*$/);
|
|
22
|
+
if (match && match[2] === filename) return match[1].toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function releaseDownloadUrl(repo, tag, filename) {
|
|
28
|
+
const ownerRepo = repo || 'arlinamid/Crucix';
|
|
29
|
+
const ref = !tag || tag === 'latest' ? 'latest' : tag;
|
|
30
|
+
const path = ref === 'latest' ? 'latest/download' : `download/${ref}`;
|
|
31
|
+
return `https://github.com/${ownerRepo}/releases/${path}/${filename}`;
|
|
32
|
+
}
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { getEnvSearchPaths } from '../apis/utils/env.mjs';
|
|
|
22
22
|
import { installUserProfile } from '../lib/profile-install.mjs';
|
|
23
23
|
import { resolveRunsDir as resolveUserRunsDir } from '../lib/user-paths.mjs';
|
|
24
24
|
import { packageVersion, versionLine } from '../lib/version.mjs';
|
|
25
|
+
import { alreadyRunningMessage, holdInstanceLock } from '../lib/instance-lock.mjs';
|
|
25
26
|
|
|
26
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
const ROOT = resolve(__dirname, '..');
|
|
@@ -1578,6 +1579,11 @@ async function main() {
|
|
|
1578
1579
|
|
|
1579
1580
|
// 0b. Continuous data collector loop without any web server
|
|
1580
1581
|
if (opts.collector) {
|
|
1582
|
+
const lock = holdInstanceLock(runsDir);
|
|
1583
|
+
if (lock.error) {
|
|
1584
|
+
console.error(alreadyRunningMessage(lock, { port, lang }));
|
|
1585
|
+
process.exit(1);
|
|
1586
|
+
}
|
|
1581
1587
|
const intervalMinutes = opts.interval || 15;
|
|
1582
1588
|
if (lang === 'hu') {
|
|
1583
1589
|
console.log('\n🔄 Crucix Folyamatos Adatgyűjtő Ciklus (Headless Collector)');
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Crucix standalone installer (Windows x64). Node is not required.
|
|
2
|
+
# irm https://github.com/arlinamid/Crucix/releases/latest/download/install.ps1 | iex
|
|
3
|
+
# Pin a tag: $env:CRUCIX_VERSION = 'v2.39.3'; irm ... | iex
|
|
4
|
+
# Dest: $env:CRUCIX_BIN (default %USERPROFILE%\.local\bin)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
$repo = if ($env:CRUCIX_REPO) { $env:CRUCIX_REPO } else { 'arlinamid/Crucix' }
|
|
8
|
+
$tag = if ($env:CRUCIX_VERSION) { $env:CRUCIX_VERSION } else { 'latest' }
|
|
9
|
+
$binDir = if ($env:CRUCIX_BIN) { $env:CRUCIX_BIN } else { Join-Path $env:USERPROFILE '.local\bin' }
|
|
10
|
+
|
|
11
|
+
$arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
|
|
12
|
+
if ($arch -eq 'AMD64') { $cpu = 'x64' }
|
|
13
|
+
elseif ($arch -eq 'ARM64') { $cpu = 'arm64' }
|
|
14
|
+
else { throw "Unsupported Windows architecture: $arch" }
|
|
15
|
+
|
|
16
|
+
$asset = "crucix-windows-$cpu.exe"
|
|
17
|
+
if ($cpu -ne 'x64') {
|
|
18
|
+
throw "This release has no Windows $cpu binary yet. Use an x64 machine or build from source (npm run install:local)."
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
$base = if ($tag -eq 'latest') {
|
|
22
|
+
"https://github.com/$repo/releases/latest/download"
|
|
23
|
+
} else {
|
|
24
|
+
if ($tag -notmatch '^v') { $tag = "v$tag" }
|
|
25
|
+
"https://github.com/$repo/releases/download/$tag"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("crucix-dl-" + [guid]::NewGuid().ToString('n'))
|
|
29
|
+
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
|
30
|
+
$exeTmp = Join-Path $tmp $asset
|
|
31
|
+
$sumsTmp = Join-Path $tmp 'SHA256SUMS.txt'
|
|
32
|
+
|
|
33
|
+
Write-Host "Crucix standalone install"
|
|
34
|
+
Write-Host " source $base/$asset"
|
|
35
|
+
Write-Host " dest $(Join-Path $binDir 'crucix.exe')"
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
try {
|
|
39
|
+
Invoke-WebRequest -Uri "$base/$asset" -OutFile $exeTmp -UseBasicParsing
|
|
40
|
+
Invoke-WebRequest -Uri "$base/SHA256SUMS.txt" -OutFile $sumsTmp -UseBasicParsing
|
|
41
|
+
} catch {
|
|
42
|
+
throw "Download failed. Check $base/$asset ($($_.Exception.Message))"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
$actual = (Get-FileHash -Algorithm SHA256 -Path $exeTmp).Hash.ToLowerInvariant()
|
|
46
|
+
$expected = $null
|
|
47
|
+
foreach ($line in Get-Content $sumsTmp) {
|
|
48
|
+
if ($line -match '^([a-fA-F0-9]{64})\s+(\S+)\s*$' -and $Matches[2] -eq $asset) {
|
|
49
|
+
$expected = $Matches[1].ToLowerInvariant()
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (-not $expected) { throw "SHA256SUMS.txt has no entry for $asset" }
|
|
54
|
+
if ($actual -ne $expected) { throw "Checksum mismatch for $asset`n expected $expected`n got $actual" }
|
|
55
|
+
Write-Host " sha256 $actual"
|
|
56
|
+
|
|
57
|
+
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
|
|
58
|
+
$target = Join-Path $binDir 'crucix.exe'
|
|
59
|
+
Copy-Item -Force $exeTmp $target
|
|
60
|
+
Set-Content -Path (Join-Path $binDir 'crucix.cmd') -Value "@echo off`r`n`"%~dp0crucix.exe`" %*`r`n" -Encoding ascii
|
|
61
|
+
$sh = "#!/usr/bin/env sh`nbasedir=`$(dirname `"`$0`")`nexec `"`$basedir/crucix.exe`" `"`$@`"`n"
|
|
62
|
+
Set-Content -Path (Join-Path $binDir 'crucix') -Value $sh -Encoding ascii
|
|
63
|
+
|
|
64
|
+
$onPath = ($env:PATH -split ';' | ForEach-Object { $_.TrimEnd('\') }) -contains $binDir.TrimEnd('\')
|
|
65
|
+
Write-Host ""
|
|
66
|
+
Write-Host "Installed $target"
|
|
67
|
+
if (-not $onPath) {
|
|
68
|
+
Write-Host "Add this folder to PATH, then open a new terminal:"
|
|
69
|
+
Write-Host " $binDir"
|
|
70
|
+
}
|
|
71
|
+
Write-Host " crucix --version"
|
|
72
|
+
Write-Host " crucix install --no-setup"
|
|
73
|
+
Write-Host " crucix setup"
|
|
74
|
+
} finally {
|
|
75
|
+
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
|
|
76
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Crucix standalone installer (Linux x64, macOS arm64). Node is not required.
|
|
3
|
+
# curl -fsSL https://github.com/arlinamid/Crucix/releases/latest/download/install.sh | sh
|
|
4
|
+
# Pin a tag: CRUCIX_VERSION=v2.39.3 curl -fsSL ... | sh
|
|
5
|
+
# Dest: CRUCIX_BIN (default ~/.local/bin)
|
|
6
|
+
|
|
7
|
+
set -eu
|
|
8
|
+
REPO="${CRUCIX_REPO:-arlinamid/Crucix}"
|
|
9
|
+
TAG="${CRUCIX_VERSION:-latest}"
|
|
10
|
+
BIN_DIR="${CRUCIX_BIN:-$HOME/.local/bin}"
|
|
11
|
+
|
|
12
|
+
os=$(uname -s)
|
|
13
|
+
arch=$(uname -m)
|
|
14
|
+
case "$os" in
|
|
15
|
+
Linux) os_name=linux ;;
|
|
16
|
+
Darwin) os_name=macos ;;
|
|
17
|
+
*) echo "Unsupported OS: $os" >&2; exit 1 ;;
|
|
18
|
+
esac
|
|
19
|
+
case "$arch" in
|
|
20
|
+
x86_64|amd64) cpu=x64 ;;
|
|
21
|
+
arm64|aarch64) cpu=arm64 ;;
|
|
22
|
+
*) echo "Unsupported architecture: $arch" >&2; exit 1 ;;
|
|
23
|
+
esac
|
|
24
|
+
|
|
25
|
+
ASSET="crucix-${os_name}-${cpu}"
|
|
26
|
+
if [ "$TAG" != latest ] && ! echo "$TAG" | grep -q '^v'; then
|
|
27
|
+
TAG="v$TAG"
|
|
28
|
+
fi
|
|
29
|
+
if [ "$TAG" = latest ]; then
|
|
30
|
+
BASE="https://github.com/${REPO}/releases/latest/download"
|
|
31
|
+
else
|
|
32
|
+
BASE="https://github.com/${REPO}/releases/download/${TAG}"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
TMP=$(mktemp -d)
|
|
36
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
37
|
+
echo "Crucix standalone install"
|
|
38
|
+
echo " source ${BASE}/${ASSET}"
|
|
39
|
+
echo " dest ${BIN_DIR}/crucix"
|
|
40
|
+
|
|
41
|
+
if ! curl -fsSL "${BASE}/${ASSET}" -o "${TMP}/${ASSET}"; then
|
|
42
|
+
echo "Download failed. This release may not have ${ASSET}." >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
curl -fsSL "${BASE}/SHA256SUMS.txt" -o "${TMP}/SHA256SUMS.txt"
|
|
46
|
+
|
|
47
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
48
|
+
ACTUAL=$(sha256sum "${TMP}/${ASSET}" | awk '{print $1}')
|
|
49
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
50
|
+
ACTUAL=$(shasum -a 256 "${TMP}/${ASSET}" | awk '{print $1}')
|
|
51
|
+
else
|
|
52
|
+
echo "Need sha256sum or shasum to verify the download." >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
EXPECTED=$(awk -v f="$ASSET" '$2==f {print $1; exit}' "${TMP}/SHA256SUMS.txt" | tr 'A-F' 'a-f')
|
|
56
|
+
if [ -z "$EXPECTED" ]; then
|
|
57
|
+
echo "SHA256SUMS.txt has no entry for ${ASSET}" >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
if [ "$ACTUAL" != "$EXPECTED" ]; then
|
|
61
|
+
echo "Checksum mismatch for ${ASSET}" >&2
|
|
62
|
+
echo " expected ${EXPECTED}" >&2
|
|
63
|
+
echo " got ${ACTUAL}" >&2
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
echo " sha256 ${ACTUAL}"
|
|
67
|
+
|
|
68
|
+
mkdir -p "$BIN_DIR"
|
|
69
|
+
cp "${TMP}/${ASSET}" "${BIN_DIR}/crucix"
|
|
70
|
+
chmod 755 "${BIN_DIR}/crucix"
|
|
71
|
+
|
|
72
|
+
echo
|
|
73
|
+
echo "Installed ${BIN_DIR}/crucix"
|
|
74
|
+
case ":$PATH:" in
|
|
75
|
+
*":${BIN_DIR}:"*) ;;
|
|
76
|
+
*) echo "Add this folder to PATH, then open a new terminal:"; echo " ${BIN_DIR}" ;;
|
|
77
|
+
esac
|
|
78
|
+
echo " crucix --version"
|
|
79
|
+
echo " crucix install --no-setup"
|
|
80
|
+
echo " crucix setup"
|
package/server.mjs
CHANGED
|
@@ -54,6 +54,8 @@ import { openFeedStore, setActiveFeedStore } from './lib/news/feed-store.mjs';
|
|
|
54
54
|
import { installFeedRoutes } from './lib/news/routes.mjs';
|
|
55
55
|
import { resolveRunsDir as resolveUserRunsDir, feedsSettingsPath, settingsEnvPath } from './lib/user-paths.mjs';
|
|
56
56
|
import { resolveDashboardPublicDir, readJarvisHtml } from './lib/dashboard-path.mjs';
|
|
57
|
+
import { alreadyRunningMessage, holdInstanceLock } from './lib/instance-lock.mjs';
|
|
58
|
+
import { isBenignHttpParserClose } from './lib/http-parser-error.mjs';
|
|
57
59
|
|
|
58
60
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
59
61
|
const ROOT = __dirname;
|
|
@@ -579,6 +581,12 @@ async function runSweepCycle() {
|
|
|
579
581
|
|
|
580
582
|
// === Startup ===
|
|
581
583
|
async function start() {
|
|
584
|
+
const lock = holdInstanceLock(RUNS_DIR);
|
|
585
|
+
if (lock.error) {
|
|
586
|
+
console.error(alreadyRunningMessage(lock, { port: config.port, lang: currentLanguage }));
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
|
|
582
590
|
const port = config.port;
|
|
583
591
|
|
|
584
592
|
const isHeadless =
|
|
@@ -672,7 +680,15 @@ async function start() {
|
|
|
672
680
|
process.on('unhandledRejection', (err) => {
|
|
673
681
|
console.error('[Crucix] Unhandled rejection:', err?.stack || err?.message || err);
|
|
674
682
|
});
|
|
683
|
+
let httpParserCloseWarned = false;
|
|
675
684
|
process.on('uncaughtException', (err) => {
|
|
685
|
+
if (isBenignHttpParserClose(err)) {
|
|
686
|
+
if (!httpParserCloseWarned) {
|
|
687
|
+
httpParserCloseWarned = true;
|
|
688
|
+
console.warn('[Crucix] Node HTTP parser hit a paused TLS socket (RSS abort). Continuing; this is not a sweep failure.');
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
676
692
|
console.error('[Crucix] Uncaught exception:', err?.stack || err?.message || err);
|
|
677
693
|
});
|
|
678
694
|
|