terminal-expose 1.0.0 → 1.2.0
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 +12 -18
- package/index.js +40 -447
- package/package.json +5 -9
- package/src/config/env.js +24 -0
- package/src/core/socket.js +56 -0
- package/src/core/terminal.js +93 -0
- package/src/core/tunnel.js +40 -0
- package/src/server/app.js +45 -0
- package/src/templates/viewer.html +482 -0
- package/src/utils/args.js +52 -0
- package/src/utils/system.js +34 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { Server } = require("socket.io");
|
|
2
|
+
const { SESSION_TOKEN } = require("../config/env");
|
|
3
|
+
|
|
4
|
+
function setupSocket(server, terminalSession) {
|
|
5
|
+
const io = new Server(server, {
|
|
6
|
+
cors: {
|
|
7
|
+
origin: false,
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
io.use((socket, next) => {
|
|
12
|
+
const token = socket.handshake.auth?.token;
|
|
13
|
+
|
|
14
|
+
if (token !== SESSION_TOKEN) {
|
|
15
|
+
return next(new Error("unauthorized"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return next();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
io.on("connection", (socket) => {
|
|
22
|
+
console.log(`Client connected: ${socket.id}`);
|
|
23
|
+
|
|
24
|
+
if (terminalSession.buffer) {
|
|
25
|
+
socket.emit("terminal-output", terminalSession.buffer);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (terminalSession.exit) {
|
|
29
|
+
socket.emit("terminal-exit", terminalSession.exit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
socket.on("terminal-input", (data) => {
|
|
33
|
+
terminalSession.write(data);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
socket.on("terminal-resize", ({ cols, rows }) => {
|
|
37
|
+
terminalSession.resize(cols, rows);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
socket.on("disconnect", () => {
|
|
41
|
+
console.log(`Client disconnected: ${socket.id}`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
terminalSession.on("data", (data) => {
|
|
46
|
+
io.emit("terminal-output", data);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
terminalSession.on("exit", (exitState) => {
|
|
50
|
+
io.emit("terminal-exit", exitState);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return io;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { setupSocket };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const pty = require("node-pty");
|
|
2
|
+
const {
|
|
3
|
+
ECHO_TO_STDOUT,
|
|
4
|
+
MAX_BUFFER,
|
|
5
|
+
TERM_COLS,
|
|
6
|
+
TERM_ROWS,
|
|
7
|
+
WORKDIR,
|
|
8
|
+
} = require("../config/env");
|
|
9
|
+
|
|
10
|
+
class TerminalSession {
|
|
11
|
+
constructor(shellCommand, shellArgs) {
|
|
12
|
+
this.buffer = "";
|
|
13
|
+
this.exit = null;
|
|
14
|
+
this.listeners = [];
|
|
15
|
+
|
|
16
|
+
this.shell = pty.spawn(shellCommand, shellArgs, {
|
|
17
|
+
name: "xterm-256color",
|
|
18
|
+
cwd: WORKDIR,
|
|
19
|
+
env: {
|
|
20
|
+
...process.env,
|
|
21
|
+
TERM: "xterm-256color",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// verify if terminal was available or not bro
|
|
26
|
+
if (process.stdin.isTTY) {
|
|
27
|
+
// listening to every stroke of host terminal input
|
|
28
|
+
process.stdin.setRawMode(true);
|
|
29
|
+
}
|
|
30
|
+
process.stdin.resume();
|
|
31
|
+
|
|
32
|
+
// write to shell after getting the input from host terminal
|
|
33
|
+
process.stdin.on("data", (data) => {
|
|
34
|
+
this.shell.write(data);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// after getting the data from shell writing it to host terminal
|
|
38
|
+
this.shell.onData((data) => {
|
|
39
|
+
if (ECHO_TO_STDOUT) {
|
|
40
|
+
// this write the user input to the host terminal
|
|
41
|
+
process.stdout.write(data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.buffer += data;
|
|
45
|
+
if (this.buffer.length > MAX_BUFFER) {
|
|
46
|
+
this.buffer = this.buffer.slice(-MAX_BUFFER);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ! need to verify it was need or not
|
|
50
|
+
this.emit("data", data);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// if the terminal was exited in host machine exit the process and inform the listeing clients
|
|
54
|
+
this.shell.onExit(({ exitCode, signal }) => {
|
|
55
|
+
this.exit = { exitCode, signal };
|
|
56
|
+
this.emit("exit", this.exit);
|
|
57
|
+
|
|
58
|
+
console.log(`Terminal-Expose exited: code=${exitCode} signal=${signal}`);
|
|
59
|
+
|
|
60
|
+
if (process.stdin.isTTY) {
|
|
61
|
+
process.stdin.setRawMode(false);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
write(data) {
|
|
67
|
+
if (this.shell) {
|
|
68
|
+
this.shell.write(data);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resize(cols, rows) {
|
|
73
|
+
if (this.shell && cols && rows) {
|
|
74
|
+
try {
|
|
75
|
+
this.shell.resize(cols, rows);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error("Failed to resize terminal", err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
on(event, callback) {
|
|
83
|
+
this.listeners.push({ event, callback });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
emit(event, data) {
|
|
87
|
+
this.listeners
|
|
88
|
+
.filter((listener) => listener.event === event)
|
|
89
|
+
.forEach((listener) => listener.callback(data));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { TerminalSession };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { startTunnel } = require("untun");
|
|
2
|
+
|
|
3
|
+
async function startPublicTunnel(options, { HOST, PORT, SESSION_TOKEN }) {
|
|
4
|
+
const localHost = HOST === "0.0.0.0" || HOST === "::" ? "127.0.0.1" : HOST;
|
|
5
|
+
|
|
6
|
+
if (options.tunnelSubdomain || options.tunnelHost) {
|
|
7
|
+
console.warn(
|
|
8
|
+
"Warning: Custom subdomain and tunnel host are not supported with Cloudflare Quick Tunnels.",
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log("Public tunnel: starting cloudflared...");
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const tunnel = await startTunnel({
|
|
16
|
+
port: PORT,
|
|
17
|
+
hostname: localHost,
|
|
18
|
+
acceptCloudflareNotice: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const url = await tunnel.getURL();
|
|
22
|
+
const publicUrl = `${url.replace(/\/$/, "")}/s/${SESSION_TOKEN}`;
|
|
23
|
+
|
|
24
|
+
console.log(`Public URL: ${publicUrl}`);
|
|
25
|
+
|
|
26
|
+
["SIGINT", "SIGTERM"].forEach((signal) => {
|
|
27
|
+
process.once(signal, async () => {
|
|
28
|
+
await tunnel.close();
|
|
29
|
+
process.kill(process.pid, signal);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return tunnel;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`Public tunnel error: ${error.message}`);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { startPublicTunnel };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { SESSION_TOKEN } = require("../config/env");
|
|
4
|
+
|
|
5
|
+
function createApp(ioGetter, terminalSession) {
|
|
6
|
+
const app = express();
|
|
7
|
+
|
|
8
|
+
// We are referring to public directory in root
|
|
9
|
+
app.use("/assets", express.static(path.join(__dirname, "../../public")));
|
|
10
|
+
|
|
11
|
+
app.get("/", (_, res) => {
|
|
12
|
+
res.type("html").send(`<!doctype html>
|
|
13
|
+
<html>
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8" />
|
|
16
|
+
<title>Terminal Expose</title>
|
|
17
|
+
</head>
|
|
18
|
+
<body style="font-family: system-ui, sans-serif; line-height: 1.5; padding: 32px;">
|
|
19
|
+
<h1>Terminal Expose</h1>
|
|
20
|
+
<p>This server is running. Use the private session URL printed in the container logs.</p>
|
|
21
|
+
</body>
|
|
22
|
+
</html>`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.get("/health", (_, res) => {
|
|
26
|
+
const io = ioGetter();
|
|
27
|
+
res.json({
|
|
28
|
+
ok: true,
|
|
29
|
+
clients: io ? io.engine.clientsCount : 0,
|
|
30
|
+
exited: terminalSession.exit,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
app.get("/s/:token", (req, res) => {
|
|
35
|
+
if (req.params.token !== SESSION_TOKEN) {
|
|
36
|
+
return res.status(403).send("Invalid session token");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
res.sendFile(path.join(__dirname, "../templates/viewer.html"));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return app;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { createApp };
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
|
|
7
|
+
<title>Terminal Session</title>
|
|
8
|
+
|
|
9
|
+
<link
|
|
10
|
+
rel="stylesheet"
|
|
11
|
+
href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css"
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
* {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html,
|
|
20
|
+
body {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 100%;
|
|
23
|
+
margin: 0;
|
|
24
|
+
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
|
|
27
|
+
background: #09090b;
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
|
|
30
|
+
font-family:
|
|
31
|
+
Inter,
|
|
32
|
+
system-ui,
|
|
33
|
+
sans-serif;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/* ======================
|
|
38
|
+
Layout
|
|
39
|
+
====================== */
|
|
40
|
+
|
|
41
|
+
.app {
|
|
42
|
+
height: 100vh;
|
|
43
|
+
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/* ======================
|
|
50
|
+
Header
|
|
51
|
+
====================== */
|
|
52
|
+
|
|
53
|
+
.header {
|
|
54
|
+
height: 48px;
|
|
55
|
+
|
|
56
|
+
padding: 0 16px;
|
|
57
|
+
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
|
|
62
|
+
background:
|
|
63
|
+
linear-gradient(
|
|
64
|
+
180deg,
|
|
65
|
+
#18181b,
|
|
66
|
+
#09090b
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
border-bottom: 1px solid #27272a;
|
|
70
|
+
|
|
71
|
+
user-select: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
.title {
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
|
|
79
|
+
color: #e4e4e7;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
.actions {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
|
|
87
|
+
gap: 14px;
|
|
88
|
+
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
/* ======================
|
|
95
|
+
Status
|
|
96
|
+
====================== */
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
.status {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
|
|
103
|
+
gap: 6px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
.status-dot {
|
|
108
|
+
width: 9px;
|
|
109
|
+
height: 9px;
|
|
110
|
+
|
|
111
|
+
border-radius: 999px;
|
|
112
|
+
|
|
113
|
+
background: currentColor;
|
|
114
|
+
|
|
115
|
+
box-shadow:
|
|
116
|
+
0 0 12px currentColor;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
.connected {
|
|
121
|
+
color: #22c55e;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
.connecting {
|
|
126
|
+
color: #f97316;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
.disconnected {
|
|
131
|
+
color: #ef4444;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/* ======================
|
|
137
|
+
Buttons
|
|
138
|
+
====================== */
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
button {
|
|
142
|
+
padding: 5px 10px;
|
|
143
|
+
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
|
|
146
|
+
background: #18181b;
|
|
147
|
+
color: #e4e4e7;
|
|
148
|
+
|
|
149
|
+
border:
|
|
150
|
+
1px solid #3f3f46;
|
|
151
|
+
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
button:hover {
|
|
157
|
+
background: #27272a;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/* ======================
|
|
164
|
+
Terminal
|
|
165
|
+
====================== */
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#terminal {
|
|
169
|
+
flex: 1;
|
|
170
|
+
|
|
171
|
+
padding: 12px;
|
|
172
|
+
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
.xterm {
|
|
178
|
+
height: 100%;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
/* ======================
|
|
184
|
+
Scrollbar
|
|
185
|
+
====================== */
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
.xterm-viewport::-webkit-scrollbar {
|
|
189
|
+
width: 8px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
.xterm-viewport::-webkit-scrollbar-track {
|
|
194
|
+
background: transparent;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
.xterm-viewport::-webkit-scrollbar-thumb {
|
|
199
|
+
background: #3f3f46;
|
|
200
|
+
|
|
201
|
+
border-radius: 20px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
|
206
|
+
background: #71717a;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@media(max-width: 600px) {
|
|
212
|
+
|
|
213
|
+
.title {
|
|
214
|
+
font-size: 12px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
.actions {
|
|
219
|
+
gap: 8px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
}
|
|
223
|
+
</style>
|
|
224
|
+
</head>
|
|
225
|
+
|
|
226
|
+
<body>
|
|
227
|
+
<div class="app">
|
|
228
|
+
<header class="header">
|
|
229
|
+
<div class="title">⚡ Remote Terminal</div>
|
|
230
|
+
|
|
231
|
+
<div class="actions">
|
|
232
|
+
<span id="timer"> 00:00 </span>
|
|
233
|
+
|
|
234
|
+
<div id="status" class="status connecting">
|
|
235
|
+
<span class="status-dot"></span>
|
|
236
|
+
|
|
237
|
+
<span id="statusText"> Connecting </span>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<button id="fullscreen">⛶</button>
|
|
241
|
+
</div>
|
|
242
|
+
</header>
|
|
243
|
+
|
|
244
|
+
<main id="terminal"></main>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
248
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
|
249
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
|
|
250
|
+
|
|
251
|
+
<script>
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
const token =
|
|
255
|
+
location.pathname
|
|
256
|
+
.split("/")
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
.pop();
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
const term =
|
|
263
|
+
new Terminal({
|
|
264
|
+
cursorBlink: true,
|
|
265
|
+
convertEol: true,
|
|
266
|
+
fontFamily:
|
|
267
|
+
`"JetBrains Mono", monospace`,
|
|
268
|
+
fontSize: 14,
|
|
269
|
+
lineHeight: 1.3,
|
|
270
|
+
scrollback: 20000,
|
|
271
|
+
theme: {
|
|
272
|
+
background: "#09090b",
|
|
273
|
+
foreground: "#fafafa",
|
|
274
|
+
cursor: "#22c55e",
|
|
275
|
+
selection: "#334155"
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
const fitAddon =
|
|
282
|
+
new FitAddon.FitAddon();
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
term.loadAddon(
|
|
287
|
+
fitAddon
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
term.open(
|
|
292
|
+
document.getElementById(
|
|
293
|
+
"terminal"
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
fitAddon.fit();
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
const socket =
|
|
303
|
+
io({
|
|
304
|
+
auth:{
|
|
305
|
+
token
|
|
306
|
+
},
|
|
307
|
+
reconnection: true,
|
|
308
|
+
reconnectionAttempts:20,
|
|
309
|
+
timeout:5000
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const statusBox =
|
|
313
|
+
document.getElementById(
|
|
314
|
+
"status"
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const statusText =
|
|
318
|
+
document.getElementById(
|
|
319
|
+
"statusText"
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
function setStatus(
|
|
324
|
+
text,
|
|
325
|
+
type
|
|
326
|
+
){
|
|
327
|
+
statusBox.className =
|
|
328
|
+
"status " + type;
|
|
329
|
+
statusText.textContent =
|
|
330
|
+
text;
|
|
331
|
+
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
socket.on(
|
|
338
|
+
"connect",
|
|
339
|
+
() => {
|
|
340
|
+
|
|
341
|
+
setStatus(
|
|
342
|
+
"Connected",
|
|
343
|
+
"connected"
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
socket.emit(
|
|
348
|
+
"terminal-resize",
|
|
349
|
+
{
|
|
350
|
+
cols: term.cols,
|
|
351
|
+
rows: term.rows
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
socket.on(
|
|
363
|
+
"disconnect",
|
|
364
|
+
() => {
|
|
365
|
+
|
|
366
|
+
setStatus(
|
|
367
|
+
"Disconnected",
|
|
368
|
+
"disconnected"
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
socket.io.on(
|
|
378
|
+
"reconnect_attempt",
|
|
379
|
+
() => {
|
|
380
|
+
|
|
381
|
+
setStatus(
|
|
382
|
+
"Reconnecting",
|
|
383
|
+
"connecting"
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
socket.on(
|
|
393
|
+
"terminal-output",
|
|
394
|
+
data => {
|
|
395
|
+
|
|
396
|
+
term.write(data);
|
|
397
|
+
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
term.onData(
|
|
406
|
+
data => {
|
|
407
|
+
|
|
408
|
+
socket.emit(
|
|
409
|
+
"terminal-input",
|
|
410
|
+
data
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
window.addEventListener(
|
|
420
|
+
"resize",
|
|
421
|
+
() => {
|
|
422
|
+
|
|
423
|
+
fitAddon.fit();
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
socket.emit(
|
|
427
|
+
"terminal-resize",
|
|
428
|
+
{
|
|
429
|
+
cols: term.cols,
|
|
430
|
+
rows: term.rows
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
let seconds = 0;
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
setInterval(
|
|
445
|
+
() => {
|
|
446
|
+
|
|
447
|
+
if(!socket.connected)
|
|
448
|
+
return;
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
seconds++;
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
timer.textContent =
|
|
455
|
+
new Date(
|
|
456
|
+
seconds * 1000
|
|
457
|
+
)
|
|
458
|
+
.toISOString()
|
|
459
|
+
.substring(
|
|
460
|
+
14,
|
|
461
|
+
19
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
},
|
|
466
|
+
1000
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
fullscreen.onclick =
|
|
474
|
+
() => {
|
|
475
|
+
|
|
476
|
+
document.documentElement
|
|
477
|
+
.requestFullscreen();
|
|
478
|
+
|
|
479
|
+
};
|
|
480
|
+
</script>
|
|
481
|
+
</body>
|
|
482
|
+
</html>
|