webtun 1.4.3 → 1.4.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 +44 -30
- package/package.json +1 -1
- package/postinstall.js +3 -3
- package/public/index.html +199 -4
- package/server.js +97 -16
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
**Access your
|
|
9
|
+
**Access your server from any browser — no VPN, no SSH client, no installing anything.**
|
|
10
10
|
|
|
11
11
|
[](install.sh)
|
|
12
12
|
[](https://github.com/unn-known1/webtun/releases/latest)
|
|
@@ -97,6 +97,17 @@ webtun
|
|
|
97
97
|
| `--help` | Show help |
|
|
98
98
|
| `--version` | Show version |
|
|
99
99
|
|
|
100
|
+
**Environment Variables:**
|
|
101
|
+
| Variable | Description |
|
|
102
|
+
|----------|-------------|
|
|
103
|
+
| `PORT` | Server port (default: 3000) |
|
|
104
|
+
| `HOST` | Bind address (default: 0.0.0.0) |
|
|
105
|
+
| `PIN` | Authentication PIN (empty = no auth) |
|
|
106
|
+
| `SHELL` | Shell to use (default: PowerShell on Windows, bash/sh elsewhere) |
|
|
107
|
+
| `WORKSPACE_ROOT` | Root directory for file operations (default: ~) |
|
|
108
|
+
| `TRUST_PROXY` | Set to `true` if behind a reverse proxy (default: loopback only) |
|
|
109
|
+
| `WEBTUN_SHELL` | Override shell on Windows (e.g., `/usr/bin/bash` for Git Bash) |
|
|
110
|
+
|
|
100
111
|
**Examples:**
|
|
101
112
|
```bash
|
|
102
113
|
npx webtun # Start on port 3000
|
|
@@ -148,35 +159,35 @@ npm start
|
|
|
148
159
|
## Architecture
|
|
149
160
|
|
|
150
161
|
```
|
|
151
|
-
|
|
152
|
-
│
|
|
153
|
-
│
|
|
154
|
-
│ │Terminal │ │
|
|
155
|
-
│ │
|
|
156
|
-
│
|
|
157
|
-
│
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
│
|
|
163
|
-
│
|
|
164
|
-
│ │ WebSocket
|
|
165
|
-
│ │
|
|
166
|
-
│
|
|
167
|
-
│
|
|
168
|
-
│
|
|
169
|
-
│
|
|
170
|
-
│ │ node-pty│ │Local Filesystem│ │ cloudflared daemon
|
|
171
|
-
│ │
|
|
172
|
-
│
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
162
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
163
|
+
│ Your Browser │
|
|
164
|
+
│ ┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐ │
|
|
165
|
+
│ │ Terminal │ │ File │ │ Tunnel │ │ Settings │ │
|
|
166
|
+
│ │ (xterm) │ │ Explorer │ │ Manager │ │ Panel │ │
|
|
167
|
+
│ └─────┬────┘ └─────┬─────┘ └──────┬──────┘ └─────┬─────┘ │
|
|
168
|
+
│ │ │ │ │ │
|
|
169
|
+
└────────┼─────────────┼───────────────┼───────────────┼─────────┘
|
|
170
|
+
│ WebSocket │ REST API │ │
|
|
171
|
+
▼ ▼ ▼ │
|
|
172
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
173
|
+
│ Node.js Server │
|
|
174
|
+
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
|
|
175
|
+
│ │ WebSocket │ │ File API │ │ Cloudflare Tunnel │ │
|
|
176
|
+
│ │ Handler │ │ (read/write) │ │ Manager │ │
|
|
177
|
+
│ └──────┬──────┘ └──────┬───────┘ └───────────┬───────────┘ │
|
|
178
|
+
│ │ │ │ │
|
|
179
|
+
│ ▼ ▼ ▼ │
|
|
180
|
+
│ ┌──────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
|
|
181
|
+
│ │ node-pty │ │ Local Filesystem │ │ cloudflared daemon │ │
|
|
182
|
+
│ │ (shell) │ │ (your server) │ │ (exposes to internet)│ │
|
|
183
|
+
│ └──────────┘ └──────────────────┘ └──────────────────────┘ │
|
|
184
|
+
└────────────────────────────────────────────────────────────────┘
|
|
185
|
+
│
|
|
186
|
+
▼
|
|
187
|
+
┌─────────────────┐
|
|
188
|
+
│ Internet Users │
|
|
189
|
+
│ (HTTPS URL) │
|
|
190
|
+
└─────────────────┘
|
|
180
191
|
```
|
|
181
192
|
|
|
182
193
|
---
|
|
@@ -231,6 +242,9 @@ kill $(pgrep -f 'cloudflared tunnel')
|
|
|
231
242
|
| Permission denied on shell | Ensure user has shell access: `chsh -s /bin/bash` |
|
|
232
243
|
| File upload fails | Check `public/uploads/` permissions: `chmod 755 public/uploads/` |
|
|
233
244
|
| Port 3000 in use | Change port: `PORT=3001 npm start` |
|
|
245
|
+
| Windows terminal opens PowerShell instead of Git Bash | Set `WEBTUN_SHELL=/usr/bin/bash` in `.env` |
|
|
246
|
+
| Behind reverse proxy, wrong client IP | Set `TRUST_PROXY=true` in `.env` |
|
|
247
|
+
| Tunnel persists after server restart | Tunnels are auto-managed; manual cleanup: `kill $(pgrep -f 'cloudflared tunnel')` |
|
|
234
248
|
|
|
235
249
|
---
|
|
236
250
|
|
package/package.json
CHANGED
package/postinstall.js
CHANGED
|
@@ -135,7 +135,7 @@ async function main() {
|
|
|
135
135
|
if (!downloadWithCurlOrWget()) {
|
|
136
136
|
console.log(' cloudflared install failed (download error) — skipping');
|
|
137
137
|
cleanup();
|
|
138
|
-
|
|
138
|
+
return;
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -149,12 +149,12 @@ async function main() {
|
|
|
149
149
|
if (/^<!doctype\s+html/i.test(head) || /^<html/i.test(head)) {
|
|
150
150
|
console.log(' cloudflared install failed (downloaded file is not a binary) — skipping');
|
|
151
151
|
cleanup();
|
|
152
|
-
|
|
152
|
+
return;
|
|
153
153
|
}
|
|
154
154
|
} catch (e) {
|
|
155
155
|
console.log(' cloudflared install failed (cannot validate download): ' + e.message);
|
|
156
156
|
cleanup();
|
|
157
|
-
|
|
157
|
+
return;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
function installBin(src, dest) {
|
package/public/index.html
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
15
15
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-search@0.15.0/lib/addon-search.min.js"></script>
|
|
16
16
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.7.0/lib/addon-unicode11.min.js"></script>
|
|
18
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.1/lib/addon-webgl.min.js"></script>
|
|
17
19
|
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.7/marked.min.js"></script>
|
|
18
20
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
|
|
19
21
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.18/lib/codemirror.min.css">
|
|
@@ -166,6 +168,8 @@ svg { display: block; }
|
|
|
166
168
|
.term-wrapper.active { display: flex; }
|
|
167
169
|
.xterm { flex: 1; }
|
|
168
170
|
.xterm-viewport { border-radius: 0; }
|
|
171
|
+
.term-bell-flash { animation: bellFlash 0.15s ease-out; }
|
|
172
|
+
@keyframes bellFlash { 0% { filter: brightness(1.8); } 100% { filter: brightness(1); } }
|
|
169
173
|
#terminals.tiles-mode { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-auto-rows: 1fr; gap: 4px; padding: 4px; position: relative; overflow: auto; min-height: 0; overscroll-behavior: none; }
|
|
170
174
|
#terminals.tiles-mode .term-wrapper { position: relative; inset: unset; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; min-height: 150px; }
|
|
171
175
|
#terminals.tiles-mode .term-wrapper.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
|
@@ -1193,6 +1197,15 @@ svg { display: block; }
|
|
|
1193
1197
|
<div class="ctx-item danger" id="ctx-delete" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> Delete</div>
|
|
1194
1198
|
</div>
|
|
1195
1199
|
|
|
1200
|
+
<!-- Terminal context menu -->
|
|
1201
|
+
<div id="term-ctx-menu" role="menu" style="position:fixed;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 4px 20px var(--shadow-color);z-index:500;display:none;min-width:160px;overflow:hidden">
|
|
1202
|
+
<div class="ctx-item" id="term-ctx-copy" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy</div>
|
|
1203
|
+
<div class="ctx-item" id="term-ctx-paste" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="14" y="2" width="8" height="8" rx="2"/><path d="M16 14H8M8 18h8"/><path d="M5 4h4"/><rect x="2" y="6" width="16" height="16" rx="2"/></svg> Paste</div>
|
|
1204
|
+
<div class="ctx-item" id="term-ctx-select-all" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/><path d="M3 9h18"/><path d="M3 15h18"/></svg> Select All</div>
|
|
1205
|
+
<div class="ctx-sep"></div>
|
|
1206
|
+
<div class="ctx-item" id="term-ctx-clear" role="menuitem"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/></svg> Clear</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
|
|
1196
1209
|
<!-- Fuzzy Finder Modal -->
|
|
1197
1210
|
<div class="overlay" id="finder-overlay">
|
|
1198
1211
|
<div class="modal modal-md" style="padding:12px">
|
|
@@ -1362,6 +1375,7 @@ let settings = {
|
|
|
1362
1375
|
cursor: 'block', blink: true, scrollback: 5000, bell: false,
|
|
1363
1376
|
mobilekeys: false, confirmclose: true, datasaver: false, autostart: false, keepAwake: false
|
|
1364
1377
|
};
|
|
1378
|
+
let serverPlatform = '';
|
|
1365
1379
|
|
|
1366
1380
|
const isElectron = !!(window.electronAPI && window.electronAPI.isElectron);
|
|
1367
1381
|
|
|
@@ -1471,6 +1485,7 @@ async function unlockApp() {
|
|
|
1471
1485
|
const info = await api('/api/home');
|
|
1472
1486
|
homeDir = info.home;
|
|
1473
1487
|
currentPath = homeDir;
|
|
1488
|
+
serverPlatform = info.platform || '';
|
|
1474
1489
|
document.getElementById('hostname-badge').textContent = info.hostname || '—';
|
|
1475
1490
|
|
|
1476
1491
|
// Check tmux availability
|
|
@@ -1819,10 +1834,77 @@ function connectWebSocket(tab, isReconnect = false) {
|
|
|
1819
1834
|
setupVisualViewport();
|
|
1820
1835
|
};
|
|
1821
1836
|
|
|
1837
|
+
// OSC sequence handler: intercepts OSC 7, 133, 52 before passing to xterm.js
|
|
1838
|
+
let oscBuf = '';
|
|
1839
|
+
function processTerminalOutput(data) {
|
|
1840
|
+
const str = typeof data === 'string' ? data : new TextDecoder().decode(data);
|
|
1841
|
+
let out = '';
|
|
1842
|
+
let i = 0;
|
|
1843
|
+
while (i < str.length) {
|
|
1844
|
+
// Look for ESC ] (OSC introducer)
|
|
1845
|
+
if (str[i] === '\x1b' && str[i + 1] === ']') {
|
|
1846
|
+
const endIdx = str.indexOf('\x07', i + 2);
|
|
1847
|
+
const stIdx = str.indexOf('\x1b\\', i + 2);
|
|
1848
|
+
let oscEnd = -1;
|
|
1849
|
+
if (endIdx !== -1 && (stIdx === -1 || endIdx < stIdx)) oscEnd = endIdx;
|
|
1850
|
+
else if (stIdx !== -1) oscEnd = stIdx;
|
|
1851
|
+
|
|
1852
|
+
if (oscEnd !== -1) {
|
|
1853
|
+
const oscData = str.substring(i + 2, oscEnd);
|
|
1854
|
+
const semi = oscData.indexOf(';');
|
|
1855
|
+
if (semi !== -1) {
|
|
1856
|
+
const code = oscData.substring(0, semi);
|
|
1857
|
+
const value = oscData.substring(semi + 1);
|
|
1858
|
+
if (code === '7') {
|
|
1859
|
+
// OSC 7: CWD update — file://hostname/path
|
|
1860
|
+
try {
|
|
1861
|
+
const url = new URL(value);
|
|
1862
|
+
tab.cwd = decodeURIComponent(url.pathname);
|
|
1863
|
+
} catch (_) {}
|
|
1864
|
+
} else if (code === '133') {
|
|
1865
|
+
// OSC 133: Shell integration markers (prompt/cmd start/end/done)
|
|
1866
|
+
// Handled silently — available for future command tracking
|
|
1867
|
+
} else if (code === '52') {
|
|
1868
|
+
// OSC 52: Clipboard operations
|
|
1869
|
+
const parts = value.split(';');
|
|
1870
|
+
const targets = parts[0] || 'c';
|
|
1871
|
+
const b64 = parts.slice(1).join(';');
|
|
1872
|
+
if (b64) {
|
|
1873
|
+
// SET clipboard
|
|
1874
|
+
try {
|
|
1875
|
+
const decoded = atob(b64);
|
|
1876
|
+
if (targets.includes('c') || targets.includes('p')) {
|
|
1877
|
+
navigator.clipboard.writeText(decoded).catch(() => {});
|
|
1878
|
+
}
|
|
1879
|
+
} catch (_) {}
|
|
1880
|
+
} else {
|
|
1881
|
+
// GET clipboard — respond with current clipboard contents
|
|
1882
|
+
navigator.clipboard.readText().then(text => {
|
|
1883
|
+
const response = '\x1b]52;c;' + btoa(text) + '\x07';
|
|
1884
|
+
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) {
|
|
1885
|
+
const enc = new TextEncoder().encode(response);
|
|
1886
|
+
const buf = new Uint8Array(1 + enc.length);
|
|
1887
|
+
buf[0] = 0x00; buf.set(enc, 1);
|
|
1888
|
+
tab.ws.send(buf.buffer);
|
|
1889
|
+
}
|
|
1890
|
+
}).catch(() => {});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
i = oscEnd + (str[oscEnd] === '\x07' ? 1 : 2);
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
out += str[i];
|
|
1899
|
+
i++;
|
|
1900
|
+
}
|
|
1901
|
+
if (out) tab.term.write(out);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1822
1904
|
ws.onmessage = e => {
|
|
1823
1905
|
const buf = new Uint8Array(e.data);
|
|
1824
1906
|
const type = buf[0], payload = buf.slice(1);
|
|
1825
|
-
if (type === 0x00)
|
|
1907
|
+
if (type === 0x00) processTerminalOutput(payload);
|
|
1826
1908
|
else if (type === 0x01) tab.term.writeln('\r\n\x1b[31m[Process exited]\x1b[0m');
|
|
1827
1909
|
else if (type === 0x02) tab.term.writeln('\r\n\x1b[31m' + new TextDecoder().decode(payload) + '\x1b[0m');
|
|
1828
1910
|
};
|
|
@@ -1919,7 +2001,7 @@ function initTerminal(tab) {
|
|
|
1919
2001
|
scrollback: settings.scrollback,
|
|
1920
2002
|
allowTransparency: false,
|
|
1921
2003
|
theme: getXtermTheme(),
|
|
1922
|
-
windowsMode:
|
|
2004
|
+
windowsMode: serverPlatform === 'win32',
|
|
1923
2005
|
convertEol: false,
|
|
1924
2006
|
bellStyle: settings.bell ? 'sound' : 'none',
|
|
1925
2007
|
smoothScrollDuration: 80,
|
|
@@ -1939,6 +2021,19 @@ function initTerminal(tab) {
|
|
|
1939
2021
|
term.loadAddon(searchAddon);
|
|
1940
2022
|
term.loadAddon(webLinksAddon);
|
|
1941
2023
|
|
|
2024
|
+
try {
|
|
2025
|
+
const unicodeAddon = new Unicode11Addon.Unicode11Addon();
|
|
2026
|
+
term.loadAddon(unicodeAddon);
|
|
2027
|
+
term.unicode.activeVersion = '11';
|
|
2028
|
+
} catch (_) {}
|
|
2029
|
+
|
|
2030
|
+
// GPU-accelerated renderer — falls back to canvas if WebGL unavailable
|
|
2031
|
+
try {
|
|
2032
|
+
const webglAddon = new WebglAddon.WebglAddon();
|
|
2033
|
+
webglAddon.onContextLoss(() => { webglAddon.dispose(); });
|
|
2034
|
+
term.loadAddon(webglAddon);
|
|
2035
|
+
} catch (_) {}
|
|
2036
|
+
|
|
1942
2037
|
term.open(tab.wrapper);
|
|
1943
2038
|
|
|
1944
2039
|
tab.term = term;
|
|
@@ -1960,6 +2055,23 @@ function initTerminal(tab) {
|
|
|
1960
2055
|
tab.el.querySelector('.tab-title').textContent = tab.title;
|
|
1961
2056
|
});
|
|
1962
2057
|
|
|
2058
|
+
// Bell: visual flash + desktop notification for background tabs
|
|
2059
|
+
term.onBell(() => {
|
|
2060
|
+
// Visual flash on terminal screen
|
|
2061
|
+
const screen = term.element?.querySelector('.xterm-screen');
|
|
2062
|
+
if (screen) {
|
|
2063
|
+
screen.classList.remove('term-bell-flash');
|
|
2064
|
+
void screen.offsetWidth; // reflow to restart animation
|
|
2065
|
+
screen.classList.add('term-bell-flash');
|
|
2066
|
+
}
|
|
2067
|
+
// Desktop notification if tab is in the background
|
|
2068
|
+
if (document.hidden && settings.bell && Notification.permission === 'granted') {
|
|
2069
|
+
try {
|
|
2070
|
+
new Notification(tab.title || 'WebTun', { body: 'Terminal bell', tag: 'webtun-bell' });
|
|
2071
|
+
} catch (_) {}
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
|
|
1963
2075
|
// Resize observer
|
|
1964
2076
|
tab.resizeObserver = new ResizeObserver(() => {
|
|
1965
2077
|
if (activeTabId === tab.id) { try { fitTerm(tab); } catch(e) { console.warn(e); } }
|
|
@@ -1979,6 +2091,55 @@ function initTerminal(tab) {
|
|
|
1979
2091
|
}
|
|
1980
2092
|
});
|
|
1981
2093
|
|
|
2094
|
+
// Terminal right-click context menu
|
|
2095
|
+
term.element.addEventListener('contextmenu', e => {
|
|
2096
|
+
e.preventDefault();
|
|
2097
|
+
const menu = document.getElementById('term-ctx-menu');
|
|
2098
|
+
const sel = term.getSelection();
|
|
2099
|
+
document.getElementById('term-ctx-copy').style.opacity = sel ? '' : '0.5';
|
|
2100
|
+
menu.style.left = Math.min(e.clientX, window.innerWidth - 170) + 'px';
|
|
2101
|
+
menu.style.top = Math.min(e.clientY, window.innerHeight - 160) + 'px';
|
|
2102
|
+
menu.style.display = 'block';
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
// Touch-to-mouse translation for TUI apps (mobile)
|
|
2106
|
+
let _tapTimeout = null, _tapPos = null, _longPressTimer = null;
|
|
2107
|
+
term.element.addEventListener('touchstart', e => {
|
|
2108
|
+
if (e.touches.length !== 1) return;
|
|
2109
|
+
const touch = e.touches[0];
|
|
2110
|
+
_tapPos = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
|
2111
|
+
// Long press detection for right-click
|
|
2112
|
+
_longPressTimer = setTimeout(() => {
|
|
2113
|
+
if (_tapPos) {
|
|
2114
|
+
const evt = new MouseEvent('mousedown', { clientX: _tapPos.x, clientY: _tapPos.y, button: 2, bubbles: true });
|
|
2115
|
+
term.element.dispatchEvent(evt);
|
|
2116
|
+
const upEvt = new MouseEvent('mouseup', { clientX: _tapPos.x, clientY: _tapPos.y, button: 2, bubbles: true });
|
|
2117
|
+
term.element.dispatchEvent(upEvt);
|
|
2118
|
+
_tapPos = null;
|
|
2119
|
+
}
|
|
2120
|
+
}, 500);
|
|
2121
|
+
}, { passive: true });
|
|
2122
|
+
term.element.addEventListener('touchend', e => {
|
|
2123
|
+
clearTimeout(_longPressTimer);
|
|
2124
|
+
if (!_tapPos || e.changedTouches.length !== 1) return;
|
|
2125
|
+
const touch = e.changedTouches[0];
|
|
2126
|
+
const dx = Math.abs(touch.clientX - _tapPos.x);
|
|
2127
|
+
const dy = Math.abs(touch.clientY - _tapPos.y);
|
|
2128
|
+
const dt = Date.now() - _tapPos.time;
|
|
2129
|
+
if (dx < 10 && dy < 10 && dt < 300) {
|
|
2130
|
+
// Short tap → left click
|
|
2131
|
+
const evt = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY, button: 0, bubbles: true });
|
|
2132
|
+
term.element.dispatchEvent(evt);
|
|
2133
|
+
const upEvt = new MouseEvent('mouseup', { clientX: touch.clientX, clientY: touch.clientY, button: 0, bubbles: true });
|
|
2134
|
+
term.element.dispatchEvent(upEvt);
|
|
2135
|
+
}
|
|
2136
|
+
_tapPos = null;
|
|
2137
|
+
}, { passive: true });
|
|
2138
|
+
term.element.addEventListener('touchmove', () => {
|
|
2139
|
+
clearTimeout(_longPressTimer);
|
|
2140
|
+
_tapPos = null;
|
|
2141
|
+
}, { passive: true });
|
|
2142
|
+
|
|
1982
2143
|
// Fit terminal then connect WebSocket — ensures WS uses correct dimensions
|
|
1983
2144
|
requestAnimationFrame(() => {
|
|
1984
2145
|
fitTerm(tab);
|
|
@@ -2110,6 +2271,10 @@ function sendKey(key) {
|
|
|
2110
2271
|
altLatch = false;
|
|
2111
2272
|
}
|
|
2112
2273
|
updateModifierButtons();
|
|
2274
|
+
} else {
|
|
2275
|
+
shiftLatch = false;
|
|
2276
|
+
altLatch = false;
|
|
2277
|
+
updateModifierButtons();
|
|
2113
2278
|
}
|
|
2114
2279
|
const tab = getActiveTab();
|
|
2115
2280
|
if (tab?.ws?.readyState === WebSocket.OPEN) {
|
|
@@ -2520,6 +2685,27 @@ function renderBreadcrumb(fullPath) {
|
|
|
2520
2685
|
// ═══════════════════════════════════════════════════════
|
|
2521
2686
|
let fsClipboard = null;
|
|
2522
2687
|
|
|
2688
|
+
// Terminal context menu actions (registered once)
|
|
2689
|
+
document.getElementById('term-ctx-copy').addEventListener('click', () => {
|
|
2690
|
+
const t = getActiveTab();
|
|
2691
|
+
if (t?.term) { const sel = t.term.getSelection(); if (sel) navigator.clipboard.writeText(sel); }
|
|
2692
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2693
|
+
});
|
|
2694
|
+
document.getElementById('term-ctx-paste').addEventListener('click', () => {
|
|
2695
|
+
pasteToTerminal();
|
|
2696
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2697
|
+
});
|
|
2698
|
+
document.getElementById('term-ctx-select-all').addEventListener('click', () => {
|
|
2699
|
+
const t = getActiveTab();
|
|
2700
|
+
if (t?.term) t.term.selectAll();
|
|
2701
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2702
|
+
});
|
|
2703
|
+
document.getElementById('term-ctx-clear').addEventListener('click', () => {
|
|
2704
|
+
const t = getActiveTab();
|
|
2705
|
+
if (t?.term) t.term.clear();
|
|
2706
|
+
document.getElementById('term-ctx-menu').style.display = 'none';
|
|
2707
|
+
});
|
|
2708
|
+
|
|
2523
2709
|
function showCtxMenu(e, file) {
|
|
2524
2710
|
e.preventDefault();
|
|
2525
2711
|
ctxTarget = file;
|
|
@@ -2552,7 +2738,11 @@ function showCtxMenu(e, file) {
|
|
|
2552
2738
|
});
|
|
2553
2739
|
}
|
|
2554
2740
|
|
|
2555
|
-
document.addEventListener('click', () =>
|
|
2741
|
+
document.addEventListener('click', () => {
|
|
2742
|
+
document.getElementById('ctx-menu').classList.remove('open');
|
|
2743
|
+
const termMenu = document.getElementById('term-ctx-menu');
|
|
2744
|
+
if (termMenu) termMenu.style.display = 'none';
|
|
2745
|
+
});
|
|
2556
2746
|
|
|
2557
2747
|
document.getElementById('ctx-open').onclick = () => { if (!ctxTarget) return; if (isImageFile(ctxTarget.path)) openImageViewer(ctxTarget.path); else loadFiles(ctxTarget.path); };
|
|
2558
2748
|
document.getElementById('ctx-edit').onclick = () => ctxTarget && openFileEditor(ctxTarget.path);
|
|
@@ -3498,6 +3688,9 @@ function toggleSetting(k) {
|
|
|
3498
3688
|
const ctrlBtn = document.getElementById('ctrl-toggle-btn');
|
|
3499
3689
|
if (ctrlBtn) ctrlBtn.style.background = '';
|
|
3500
3690
|
}
|
|
3691
|
+
if (k === 'bell' && settings.bell && 'Notification' in window && Notification.permission === 'default') {
|
|
3692
|
+
Notification.requestPermission();
|
|
3693
|
+
}
|
|
3501
3694
|
if (k === 'datasaver') {
|
|
3502
3695
|
if (settings.datasaver) {
|
|
3503
3696
|
if (sysStatsTimer) { clearInterval(sysStatsTimer); sysStatsTimer = null; }
|
|
@@ -4151,7 +4344,9 @@ function pasteToTerminal() {
|
|
|
4151
4344
|
if (!text) return;
|
|
4152
4345
|
const tab = getActiveTab();
|
|
4153
4346
|
if (tab?.ws && tab.ws.readyState === WebSocket.OPEN) {
|
|
4154
|
-
|
|
4347
|
+
// Bracketed paste: wrap in escape sequences so the shell buffers the input
|
|
4348
|
+
const bracketed = '\x1b[200~' + text + '\x1b[201~';
|
|
4349
|
+
const enc = new TextEncoder().encode(bracketed);
|
|
4155
4350
|
const buf = new Uint8Array(1 + enc.length);
|
|
4156
4351
|
buf[0] = 0x00; buf.set(enc, 1);
|
|
4157
4352
|
tab.ws.send(buf.buffer);
|
package/server.js
CHANGED
|
@@ -8,7 +8,10 @@ try {
|
|
|
8
8
|
const idx = trimmed.indexOf('=');
|
|
9
9
|
if (idx === -1) return;
|
|
10
10
|
const key = trimmed.slice(0, idx).trim();
|
|
11
|
-
|
|
11
|
+
let val = trimmed.slice(idx + 1).trim();
|
|
12
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
13
|
+
val = val.slice(1, -1);
|
|
14
|
+
}
|
|
12
15
|
if (!(key in process.env)) process.env[key] = val;
|
|
13
16
|
});
|
|
14
17
|
} catch {}
|
|
@@ -114,7 +117,10 @@ const wss = new WebSocket.Server({ server, path: '/ws' });
|
|
|
114
117
|
|
|
115
118
|
const PORT = process.env.PORT || 3000;
|
|
116
119
|
const PIN = process.env.PIN || '';
|
|
117
|
-
|
|
120
|
+
// On Windows, ignore SHELL env from Git Bash/MSYS2/WSL — prefer PowerShell
|
|
121
|
+
const SHELL = (os.platform() === 'win32' && !process.env.WEBTUN_SHELL)
|
|
122
|
+
? 'powershell.exe'
|
|
123
|
+
: (process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : (fs.existsSync('/bin/bash') ? '/bin/bash' : 'sh')));
|
|
118
124
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
119
125
|
const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT ? path.resolve(process.env.WORKSPACE_ROOT) : os.homedir();
|
|
120
126
|
|
|
@@ -132,7 +138,9 @@ app.use((req, res, next) => {
|
|
|
132
138
|
});
|
|
133
139
|
|
|
134
140
|
// Trust proxy for proper IP detection behind reverse proxy
|
|
135
|
-
|
|
141
|
+
// 'loopback' only trusts 127.0.0.1/::1 — safe default for direct connections
|
|
142
|
+
// Set TRUST_PROXY=true in .env if behind a reverse proxy
|
|
143
|
+
app.set('trust proxy', process.env.TRUST_PROXY === 'true' ? 1 : 'loopback');
|
|
136
144
|
|
|
137
145
|
function constantTimeEqual(a, b) {
|
|
138
146
|
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
@@ -161,7 +169,7 @@ app.post('/api/auth', authRateLimiter, (req, res) => {
|
|
|
161
169
|
if (!PIN || (pin && constantTimeEqual(pin, PIN))) {
|
|
162
170
|
res.json({ success: true, token: PIN || 'open' });
|
|
163
171
|
} else {
|
|
164
|
-
res.status(401).json({ error: '
|
|
172
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
165
173
|
}
|
|
166
174
|
});
|
|
167
175
|
|
|
@@ -332,7 +340,13 @@ function killPid(pid, signal = 'SIGTERM') {
|
|
|
332
340
|
if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) return;
|
|
333
341
|
try {
|
|
334
342
|
if (os.platform() === 'win32') {
|
|
335
|
-
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' });
|
|
343
|
+
try { execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' }); } catch {}
|
|
344
|
+
// Fallback: also kill any remaining child processes via WMIC
|
|
345
|
+
try {
|
|
346
|
+
const out = execSync(`wmic process where "ParentProcessId=${pid}" get ProcessId /format:list`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
347
|
+
const childPids = out.split('\n').filter(l => l.startsWith('ProcessId=')).map(l => parseInt(l.split('=')[1])).filter(Boolean);
|
|
348
|
+
for (const cp of childPids) { try { execSync(`taskkill /PID ${cp} /F`, { stdio: 'ignore' }); } catch {} }
|
|
349
|
+
} catch {}
|
|
336
350
|
} else {
|
|
337
351
|
process.kill(pid, signal);
|
|
338
352
|
}
|
|
@@ -344,7 +358,7 @@ function buildSessionEnv() {
|
|
|
344
358
|
const env = { ...process.env };
|
|
345
359
|
env.TERM = env.TERM || 'xterm-256color';
|
|
346
360
|
env.COLORTERM = env.COLORTERM || 'truecolor';
|
|
347
|
-
if (!env.HOME && env.USERPROFILE) env.HOME = env.USERPROFILE;
|
|
361
|
+
if (!env.HOME && env.USERPROFILE) env.HOME = env.USERPROFILE.replace(/\\/g, '/');
|
|
348
362
|
if (!env.USER && env.USERNAME) env.USER = env.USERNAME;
|
|
349
363
|
env.SHELL = SHELL;
|
|
350
364
|
// Prefer Path (Windows) over PATH if both set
|
|
@@ -747,7 +761,7 @@ app.post('/api/files/upload', checkPin, (req, res) => {
|
|
|
747
761
|
cb(err);
|
|
748
762
|
}
|
|
749
763
|
},
|
|
750
|
-
filename: (_, file, cb) => cb(null, path.basename(file.originalname).replace(/[
|
|
764
|
+
filename: (_, file, cb) => cb(null, path.basename(file.originalname).replace(/[^\w\u00C0-\u024F\u0400-\u04FF\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF_.\-]/g, '_'))
|
|
751
765
|
});
|
|
752
766
|
const upload = multer({ storage, limits: { fileSize: 500 * 1024 * 1024, files: 100 } }).array('files');
|
|
753
767
|
upload(req, res, err => {
|
|
@@ -779,10 +793,22 @@ app.get('/api/files/stat', checkPin, async (req, res) => {
|
|
|
779
793
|
isSocket: st.isSocket(), isFIFO: st.isFIFO(),
|
|
780
794
|
};
|
|
781
795
|
try {
|
|
782
|
-
|
|
796
|
+
if (os.platform() === 'win32') {
|
|
797
|
+
stat.owner = String(st.uid);
|
|
798
|
+
} else {
|
|
799
|
+
stat.owner = execFileSync('id', ['-nu', String(st.uid)], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
800
|
+
}
|
|
783
801
|
} catch { stat.owner = String(st.uid); }
|
|
784
802
|
try {
|
|
785
|
-
|
|
803
|
+
if (os.platform() === 'win32') {
|
|
804
|
+
stat.group = String(st.gid);
|
|
805
|
+
} else if (os.platform() === 'darwin') {
|
|
806
|
+
const dscl = execSync(`dscl . -read /Groups/${st.gid} RecordName 2>/dev/null`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
807
|
+
const m = dscl.match(/RecordName:\s*(.+)/);
|
|
808
|
+
stat.group = m ? m[1].trim() : String(st.gid);
|
|
809
|
+
} else {
|
|
810
|
+
stat.group = execFileSync('getent', ['group', String(st.gid)], { encoding: 'utf8', stdio: 'pipe' }).split(':')[0];
|
|
811
|
+
}
|
|
786
812
|
} catch { stat.group = String(st.gid); }
|
|
787
813
|
try {
|
|
788
814
|
const symlink = lst && lst.isSymbolicLink() ? await fsPromises.readlink(p) : null;
|
|
@@ -873,7 +899,8 @@ app.post('/api/files/chmod', checkPin, async (req, res) => {
|
|
|
873
899
|
if (!/^[0-7]{3,4}$/.test(req.body.mode)) return res.status(400).json({ error: 'mode must be a 3-4 digit octal number (e.g. 755, 644, 1777)' });
|
|
874
900
|
const mode = parseInt(req.body.mode, 8);
|
|
875
901
|
await fsPromises.chmod(p, mode);
|
|
876
|
-
|
|
902
|
+
const warning = os.platform() === 'win32' ? 'chmod has no effect on Windows' : undefined;
|
|
903
|
+
res.json({ success: true, mode: req.body.mode, ...(warning && { warning }) });
|
|
877
904
|
} catch (e) {
|
|
878
905
|
res.status(500).json({ error: e.message });
|
|
879
906
|
}
|
|
@@ -1071,10 +1098,21 @@ app.get('/api/system/network', checkPin, async (req, res) => {
|
|
|
1071
1098
|
gateway = route.split('\n')[0].trim() || null;
|
|
1072
1099
|
const dnsOut = execFileSync('powershell.exe', ['-Command', '(Get-DnsClientServerAddress -AddressFamily IPv4).ServerAddresses'], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1073
1100
|
dns = dnsOut.split('\n').filter(Boolean);
|
|
1101
|
+
} else if (os.platform() === 'darwin') {
|
|
1102
|
+
const route = execFileSync('sh', ['-c', "route -n get default 2>/dev/null | awk '/gateway:/{print $2}'"], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1103
|
+
gateway = route || null;
|
|
1104
|
+
const resolv = execFileSync('sh', ['-c', "scutil --dns 2>/dev/null | awk '/nameserver\[0\]/{print $3}' | head -3"], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1105
|
+
dns = resolv.split('\n').filter(Boolean);
|
|
1074
1106
|
} else {
|
|
1075
1107
|
const route = execFileSync('sh', ['-c', "ip route | grep default | head -1 | awk '{print $3}'"], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1076
1108
|
gateway = route || null;
|
|
1077
|
-
|
|
1109
|
+
let resolv = '';
|
|
1110
|
+
try {
|
|
1111
|
+
resolv = execFileSync('sh', ['-c', "grep nameserver /etc/resolv.conf | awk '{print $2}'"], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1112
|
+
} catch {}
|
|
1113
|
+
if (!resolv) {
|
|
1114
|
+
try { resolv = execFileSync('sh', ['-c', "resolvectl status 2>/dev/null | awk '/DNS Servers/{found=1; next} /^$/{found=0} found{print $1}' | head -3"], { encoding: 'utf8', stdio: 'pipe' }).trim(); } catch {}
|
|
1115
|
+
}
|
|
1078
1116
|
dns = resolv.split('\n').filter(Boolean);
|
|
1079
1117
|
}
|
|
1080
1118
|
} catch {}
|
|
@@ -1085,6 +1123,15 @@ app.get('/api/system/network', checkPin, async (req, res) => {
|
|
|
1085
1123
|
const m = l.match(/:(\d+)\s+/);
|
|
1086
1124
|
return m ? { port: parseInt(m[1]), process: l.split(/\s+/).pop() } : null;
|
|
1087
1125
|
}).filter(Boolean);
|
|
1126
|
+
} else if (os.platform() === 'darwin') {
|
|
1127
|
+
const out = execFileSync('lsof', ['-i', '-P', '-n', '-sTCP:LISTEN'], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1128
|
+
const lines = out.split('\n').slice(1).filter(Boolean);
|
|
1129
|
+
listenPorts = lines.map(l => {
|
|
1130
|
+
const parts = l.split(/\s+/);
|
|
1131
|
+
const addr = parts[8] || '';
|
|
1132
|
+
const m = addr.match(/:(\d+)$/);
|
|
1133
|
+
return m ? { port: parseInt(m[1]), address: addr, process: parts[0] || '' } : null;
|
|
1134
|
+
}).filter(Boolean);
|
|
1088
1135
|
} else {
|
|
1089
1136
|
const out = execFileSync('sh', ['-c', "ss -tlnp 2>/dev/null | tail -n+2"], { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1090
1137
|
listenPorts = out.split('\n').filter(Boolean).map(l => {
|
|
@@ -1282,7 +1329,8 @@ wss.on('connection', (ws, req) => {
|
|
|
1282
1329
|
});
|
|
1283
1330
|
}
|
|
1284
1331
|
} else {
|
|
1285
|
-
|
|
1332
|
+
const shellArgs = os.platform() === 'win32' ? ['-NoLogo'] : ['-l'];
|
|
1333
|
+
proc = pty.spawn(SHELL, shellArgs, {
|
|
1286
1334
|
name: 'xterm-256color', cols, rows, cwd,
|
|
1287
1335
|
env: sessionEnv
|
|
1288
1336
|
});
|
|
@@ -1293,9 +1341,29 @@ wss.on('connection', (ws, req) => {
|
|
|
1293
1341
|
return;
|
|
1294
1342
|
}
|
|
1295
1343
|
|
|
1296
|
-
|
|
1344
|
+
// Back-pressure: pause PTY output when WebSocket send buffer is full
|
|
1345
|
+
let paused = false;
|
|
1346
|
+
const HIGH_WATER = 4 * 1024 * 1024; // 4MB — pause PTY above this
|
|
1347
|
+
const LOW_WATER = 1 * 1024 * 1024; // 1MB — resume PTY below this
|
|
1348
|
+
|
|
1349
|
+
proc.onData(data => {
|
|
1350
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
1351
|
+
send(0x00, data);
|
|
1352
|
+
// If WebSocket buffer is backing up, pause PTY to prevent OOM
|
|
1353
|
+
if (!paused && ws.bufferedAmount > HIGH_WATER) {
|
|
1354
|
+
try { proc.pause(); paused = true; } catch (_) {}
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
// Drain check: resume PTY when buffer drops
|
|
1359
|
+
const drainCheck = setInterval(() => {
|
|
1360
|
+
if (paused && ws.bufferedAmount < LOW_WATER) {
|
|
1361
|
+
try { proc.resume(); paused = false; } catch (_) {}
|
|
1362
|
+
}
|
|
1363
|
+
}, 50);
|
|
1297
1364
|
|
|
1298
1365
|
proc.onExit(() => {
|
|
1366
|
+
clearInterval(drainCheck);
|
|
1299
1367
|
if (!TMUX || !sessionId) send(0x01, Buffer.from([0]));
|
|
1300
1368
|
ws.close();
|
|
1301
1369
|
});
|
|
@@ -1314,8 +1382,8 @@ wss.on('connection', (ws, req) => {
|
|
|
1314
1382
|
if (buf.length < 1) return;
|
|
1315
1383
|
const type = buf[0];
|
|
1316
1384
|
if (type === 0x00) {
|
|
1317
|
-
// Limit input to
|
|
1318
|
-
const payload = buf.slice(1, Math.min(buf.length,
|
|
1385
|
+
// Limit input to 1MB per message
|
|
1386
|
+
const payload = buf.slice(1, Math.min(buf.length, 1048577));
|
|
1319
1387
|
proc.write(payload.toString('utf8'));
|
|
1320
1388
|
} else if (type === 0x01 && buf.length >= 5) {
|
|
1321
1389
|
const c = buf.readUInt16LE(1), r = buf.readUInt16LE(3);
|
|
@@ -1331,6 +1399,7 @@ wss.on('connection', (ws, req) => {
|
|
|
1331
1399
|
|
|
1332
1400
|
const cleanup = () => {
|
|
1333
1401
|
clearInterval(pingInterval);
|
|
1402
|
+
clearInterval(drainCheck);
|
|
1334
1403
|
try { proc.kill(); } catch {}
|
|
1335
1404
|
};
|
|
1336
1405
|
ws.on('close', cleanup);
|
|
@@ -1448,6 +1517,15 @@ app.get('/api/system', checkPin, async (req, res) => {
|
|
|
1448
1517
|
mem: p.mem || '',
|
|
1449
1518
|
cmd: p.cmd || ''
|
|
1450
1519
|
}));
|
|
1520
|
+
} else if (os.platform() === 'darwin') {
|
|
1521
|
+
const psOut = await spawnRead('ps', ['-axo', 'pid,user,%cpu,%mem,command', '-r']);
|
|
1522
|
+
const lines = psOut.trim().split('\n').slice(1, 16);
|
|
1523
|
+
for (const line of lines) {
|
|
1524
|
+
const m = line.match(/^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)/);
|
|
1525
|
+
if (m) {
|
|
1526
|
+
processes.push({ user: m[2], pid: m[1], cpu: m[3], mem: m[4], cmd: m[5] });
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1451
1529
|
} else {
|
|
1452
1530
|
const psOut = await spawnRead('ps', ['-eo', 'pid,user,%cpu,%mem,cmd', '--no-headers', '--sort=-%cpu']);
|
|
1453
1531
|
const lines = psOut.trim().split('\n').slice(0, 15);
|
|
@@ -1482,9 +1560,12 @@ function isCloudflaredProcess(pid) {
|
|
|
1482
1560
|
if (os.platform() === 'win32') {
|
|
1483
1561
|
const stdout = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
1484
1562
|
return stdout.toLowerCase().includes('cloudflared');
|
|
1485
|
-
} else {
|
|
1563
|
+
} else if (os.platform() === 'linux') {
|
|
1486
1564
|
const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8');
|
|
1487
1565
|
return cmdline.toLowerCase().includes('cloudflared');
|
|
1566
|
+
} else {
|
|
1567
|
+
const stdout = execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
1568
|
+
return stdout.toLowerCase().includes('cloudflared');
|
|
1488
1569
|
}
|
|
1489
1570
|
} catch {
|
|
1490
1571
|
return false;
|