fix: detect rate-limit error, auto-retry once after 30s with friendly message
Browse files- app.js +37 -5
- public/app.js +13 -2
- public/style.css +1 -0
app.js
CHANGED
|
@@ -107,8 +107,10 @@ function broadcast(payload) {
|
|
| 107 |
}
|
| 108 |
}
|
| 109 |
|
|
|
|
|
|
|
| 110 |
function startShellular() {
|
| 111 |
-
if (shellularProc) return;
|
| 112 |
|
| 113 |
broadcast({ type: 'status', status: 'starting' });
|
| 114 |
|
|
@@ -117,8 +119,12 @@ function startShellular() {
|
|
| 117 |
stdio: ['ignore', 'pipe', 'pipe'],
|
| 118 |
});
|
| 119 |
|
|
|
|
|
|
|
|
|
|
| 120 |
const handleData = (chunk) => {
|
| 121 |
const text = stripAnsi(chunk.toString());
|
|
|
|
| 122 |
outputBuffer += text;
|
| 123 |
broadcast({ type: 'output', text });
|
| 124 |
};
|
|
@@ -135,17 +141,43 @@ function startShellular() {
|
|
| 135 |
});
|
| 136 |
|
| 137 |
shellularProc.on('exit', (code, signal) => {
|
| 138 |
-
const text = `\n[shellular exited — code=${code ?? '?'}, signal=${signal ?? 'none'}]\n`;
|
| 139 |
-
outputBuffer += text;
|
| 140 |
-
broadcast({ type: 'output', text });
|
| 141 |
shellularProc = null;
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
});
|
| 144 |
|
| 145 |
broadcast({ type: 'status', status: 'running' });
|
| 146 |
}
|
| 147 |
|
| 148 |
function stopShellular() {
|
|
|
|
| 149 |
if (!shellularProc) return;
|
| 150 |
shellularProc.kill('SIGTERM');
|
| 151 |
shellularProc = null;
|
|
|
|
| 107 |
}
|
| 108 |
}
|
| 109 |
|
| 110 |
+
let retryTimer = null;
|
| 111 |
+
|
| 112 |
function startShellular() {
|
| 113 |
+
if (shellularProc || retryTimer) return;
|
| 114 |
|
| 115 |
broadcast({ type: 'status', status: 'starting' });
|
| 116 |
|
|
|
|
| 119 |
stdio: ['ignore', 'pipe', 'pipe'],
|
| 120 |
});
|
| 121 |
|
| 122 |
+
// Accumulate stdout/stderr so we can detect the error type on exit
|
| 123 |
+
let procOutput = '';
|
| 124 |
+
|
| 125 |
const handleData = (chunk) => {
|
| 126 |
const text = stripAnsi(chunk.toString());
|
| 127 |
+
procOutput += text;
|
| 128 |
outputBuffer += text;
|
| 129 |
broadcast({ type: 'output', text });
|
| 130 |
};
|
|
|
|
| 141 |
});
|
| 142 |
|
| 143 |
shellularProc.on('exit', (code, signal) => {
|
|
|
|
|
|
|
|
|
|
| 144 |
shellularProc = null;
|
| 145 |
+
|
| 146 |
+
// Detect rate-limit / registration failure (exit code 1, no signal)
|
| 147 |
+
const isRegError = code === 1 && !signal &&
|
| 148 |
+
(procOutput.includes('invalid_union') || procOutput.includes('Too many requests') ||
|
| 149 |
+
procOutput.includes('host registration'));
|
| 150 |
+
|
| 151 |
+
if (isRegError) {
|
| 152 |
+
const WAIT = 30;
|
| 153 |
+
const msg = `\n⚠ Registration rate-limited by shellular API.\n` +
|
| 154 |
+
` Retrying automatically in ${WAIT}s — please wait…\n`;
|
| 155 |
+
outputBuffer += msg;
|
| 156 |
+
broadcast({ type: 'output', text: msg });
|
| 157 |
+
broadcast({ type: 'status', status: 'retrying' });
|
| 158 |
+
|
| 159 |
+
retryTimer = setTimeout(() => {
|
| 160 |
+
retryTimer = null;
|
| 161 |
+
const msg2 = '\n[Retrying registration…]\n';
|
| 162 |
+
outputBuffer += msg2;
|
| 163 |
+
broadcast({ type: 'output', text: msg2 });
|
| 164 |
+
startShellular();
|
| 165 |
+
}, WAIT * 1000);
|
| 166 |
+
} else {
|
| 167 |
+
const text = code !== 0
|
| 168 |
+
? `\n[shellular exited — code=${code ?? '?'}, signal=${signal ?? 'none'}]\n`
|
| 169 |
+
: '\n[shellular disconnected]\n';
|
| 170 |
+
outputBuffer += text;
|
| 171 |
+
broadcast({ type: 'output', text });
|
| 172 |
+
broadcast({ type: 'status', status: 'stopped' });
|
| 173 |
+
}
|
| 174 |
});
|
| 175 |
|
| 176 |
broadcast({ type: 'status', status: 'running' });
|
| 177 |
}
|
| 178 |
|
| 179 |
function stopShellular() {
|
| 180 |
+
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
|
| 181 |
if (!shellularProc) return;
|
| 182 |
shellularProc.kill('SIGTERM');
|
| 183 |
shellularProc = null;
|
public/app.js
CHANGED
|
@@ -184,9 +184,20 @@ function handleEvent(payload) {
|
|
| 184 |
function updateStatus(status) {
|
| 185 |
shellStatus = status;
|
| 186 |
|
| 187 |
-
const labels = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
statusBadge.textContent = labels[status] || status;
|
| 189 |
-
statusBadge.className
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
if (status === 'stopped' || status === 'error') {
|
| 192 |
if (!qrRendered) {
|
|
|
|
| 184 |
function updateStatus(status) {
|
| 185 |
shellStatus = status;
|
| 186 |
|
| 187 |
+
const labels = {
|
| 188 |
+
running: 'Running',
|
| 189 |
+
starting: 'Starting',
|
| 190 |
+
retrying: 'Retrying…',
|
| 191 |
+
stopped: 'Stopped',
|
| 192 |
+
error: 'Error',
|
| 193 |
+
};
|
| 194 |
statusBadge.textContent = labels[status] || status;
|
| 195 |
+
statusBadge.className = `badge badge-${status}`;
|
| 196 |
+
|
| 197 |
+
if (status === 'retrying') {
|
| 198 |
+
if (!qrRendered) setQrState('loading');
|
| 199 |
+
return; // keep showing the spinner while we wait
|
| 200 |
+
}
|
| 201 |
|
| 202 |
if (status === 'stopped' || status === 'error') {
|
| 203 |
if (!qrRendered) {
|
public/style.css
CHANGED
|
@@ -242,6 +242,7 @@ html, body {
|
|
| 242 |
}
|
| 243 |
.badge-running { background: rgba(74,222,128,.15); color: var(--success); }
|
| 244 |
.badge-starting { background: rgba(250,204,21,.15); color: var(--warning); }
|
|
|
|
| 245 |
.badge-stopped { background: rgba(255,95,109,.12); color: var(--error); }
|
| 246 |
.badge-error { background: rgba(255,95,109,.12); color: var(--error); }
|
| 247 |
|
|
|
|
| 242 |
}
|
| 243 |
.badge-running { background: rgba(74,222,128,.15); color: var(--success); }
|
| 244 |
.badge-starting { background: rgba(250,204,21,.15); color: var(--warning); }
|
| 245 |
+
.badge-retrying { background: rgba(250,204,21,.15); color: var(--warning); }
|
| 246 |
.badge-stopped { background: rgba(255,95,109,.12); color: var(--error); }
|
| 247 |
.badge-error { background: rgba(255,95,109,.12); color: var(--error); }
|
| 248 |
|