ownremote 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +211 -2
- package/cli.js +843 -0
- package/package.json +49 -4
- package/public/app.js +957 -0
- package/public/index.html +211 -0
- package/public/style.css +318 -0
- package/src/admin-client.js +64 -0
- package/src/config.js +125 -0
- package/src/daemon.js +43 -0
- package/src/input-helper.ps1 +163 -0
- package/src/input.js +152 -0
- package/src/launch-hidden.vbs +13 -0
- package/src/logo.js +137 -0
- package/src/screen-helper.ps1 +127 -0
- package/src/screen.js +285 -0
- package/src/server.js +567 -0
- package/src/startup.js +74 -0
- package/src/tray-helper.ps1 +183 -0
- package/src/tray.js +85 -0
- package/src/tunnel.js +129 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Ikon system tray untuk mode background. Dijalankan oleh src/tray.js.
|
|
2
|
+
# stdin (dari node), satu perintah per baris:
|
|
3
|
+
# state <url>\t<code>\t<online>\t<paired>\t<allowPairing 1|0>\t<publicUrl>
|
|
4
|
+
# notify <judul>\t<teks>
|
|
5
|
+
# quit
|
|
6
|
+
# stdout (ke node): "ready", "cmd open", "cmd newcode", "cmd togglepair", "cmd stop"
|
|
7
|
+
# Jika stdin ditutup (server mati), ikon dihapus dan proses berhenti.
|
|
8
|
+
param([string]$Render)
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
|
|
11
|
+
Add-Type -ReferencedAssemblies System.Windows.Forms, System.Drawing -TypeDefinition @"
|
|
12
|
+
using System;
|
|
13
|
+
using System.Drawing;
|
|
14
|
+
using System.Drawing.Drawing2D;
|
|
15
|
+
using System.Runtime.InteropServices;
|
|
16
|
+
using System.Threading;
|
|
17
|
+
using System.Windows.Forms;
|
|
18
|
+
|
|
19
|
+
public static class OwnRemoteTray {
|
|
20
|
+
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
|
|
21
|
+
[DllImport("user32.dll")] static extern bool DestroyIcon(IntPtr handle);
|
|
22
|
+
|
|
23
|
+
static NotifyIcon icon;
|
|
24
|
+
static Control ui;
|
|
25
|
+
static ToolStripMenuItem urlItem, publicItem, codeItem, devicesItem, pairItem;
|
|
26
|
+
static Icon idleIcon, activeIcon;
|
|
27
|
+
static string url = "", publicUrl = "", code = "";
|
|
28
|
+
|
|
29
|
+
static void Emit(string s) {
|
|
30
|
+
Console.Out.WriteLine(s);
|
|
31
|
+
Console.Out.Flush();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Panah cursor hijau limau dengan outline gelap; titik hijau = ada perangkat terhubung.
|
|
35
|
+
public static Bitmap Draw(bool active, int size) {
|
|
36
|
+
var bmp = new Bitmap(size, size);
|
|
37
|
+
float k = size / 32f;
|
|
38
|
+
using (var g = Graphics.FromImage(bmp)) {
|
|
39
|
+
g.SmoothingMode = SmoothingMode.AntiAlias;
|
|
40
|
+
g.Clear(Color.Transparent);
|
|
41
|
+
var pts = new PointF[] {
|
|
42
|
+
new PointF(5 * k, 2 * k), new PointF(5 * k, 26 * k), new PointF(11 * k, 20.5f * k),
|
|
43
|
+
new PointF(15 * k, 29.5f * k), new PointF(19.5f * k, 27.5f * k), new PointF(15.5f * k, 18.5f * k),
|
|
44
|
+
new PointF(23.5f * k, 18.5f * k)
|
|
45
|
+
};
|
|
46
|
+
using (var outline = new Pen(Color.FromArgb(255, 13, 15, 20), 3.2f * k)) {
|
|
47
|
+
outline.LineJoin = LineJoin.Round;
|
|
48
|
+
g.DrawPolygon(outline, pts);
|
|
49
|
+
}
|
|
50
|
+
using (var fill = new SolidBrush(Color.FromArgb(255, 198, 244, 91))) g.FillPolygon(fill, pts);
|
|
51
|
+
if (active) {
|
|
52
|
+
float d = 13 * k, x = size - d - 0.5f * k, y = size - d - 0.5f * k;
|
|
53
|
+
using (var ring = new SolidBrush(Color.FromArgb(255, 13, 15, 20))) g.FillEllipse(ring, x - 1.5f * k, y - 1.5f * k, d + 3 * k, d + 3 * k);
|
|
54
|
+
using (var dot = new SolidBrush(Color.FromArgb(255, 74, 222, 128))) g.FillEllipse(dot, x, y, d, d);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return bmp;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static Icon MakeIcon(bool active) {
|
|
61
|
+
int size = Math.Max(16, SystemInformation.SmallIconSize.Width);
|
|
62
|
+
using (var bmp = Draw(active, size)) {
|
|
63
|
+
IntPtr h = bmp.GetHicon();
|
|
64
|
+
var tmp = Icon.FromHandle(h);
|
|
65
|
+
var copy = (Icon)tmp.Clone();
|
|
66
|
+
DestroyIcon(h);
|
|
67
|
+
return copy;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public static void Run() {
|
|
72
|
+
try { SetProcessDPIAware(); } catch { }
|
|
73
|
+
Application.EnableVisualStyles();
|
|
74
|
+
ui = new Control();
|
|
75
|
+
var handle = ui.Handle; // paksa handle dibuat agar BeginInvoke bisa dipakai dari thread pembaca
|
|
76
|
+
|
|
77
|
+
idleIcon = MakeIcon(false);
|
|
78
|
+
activeIcon = MakeIcon(true);
|
|
79
|
+
|
|
80
|
+
var menu = new ContextMenuStrip();
|
|
81
|
+
var title = new ToolStripMenuItem("OwnRemote - running in background");
|
|
82
|
+
title.Enabled = false;
|
|
83
|
+
urlItem = new ToolStripMenuItem("Open on phone: -", null, (s, e) => Copy(url, "Address copied"));
|
|
84
|
+
publicItem = new ToolStripMenuItem("Internet: -", null, (s, e) => Copy(publicUrl, "Public URL copied"));
|
|
85
|
+
publicItem.Visible = false;
|
|
86
|
+
codeItem = new ToolStripMenuItem("Pairing code: -", null, (s, e) => Copy(code, "Pairing code copied"));
|
|
87
|
+
devicesItem = new ToolStripMenuItem("Devices: -");
|
|
88
|
+
devicesItem.Enabled = false;
|
|
89
|
+
var open = new ToolStripMenuItem("Open OwnRemote menu", null, (s, e) => Emit("cmd open"));
|
|
90
|
+
open.Font = new Font(open.Font, FontStyle.Bold);
|
|
91
|
+
var newCode = new ToolStripMenuItem("Generate new pairing code", null, (s, e) => Emit("cmd newcode"));
|
|
92
|
+
pairItem = new ToolStripMenuItem("Lock pairing", null, (s, e) => Emit("cmd togglepair"));
|
|
93
|
+
var stop = new ToolStripMenuItem("Stop server", null, (s, e) => Emit("cmd stop"));
|
|
94
|
+
menu.Items.AddRange(new ToolStripItem[] {
|
|
95
|
+
title, new ToolStripSeparator(),
|
|
96
|
+
urlItem, publicItem, codeItem, devicesItem, new ToolStripSeparator(),
|
|
97
|
+
open, newCode, pairItem, new ToolStripSeparator(),
|
|
98
|
+
stop
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
icon = new NotifyIcon();
|
|
102
|
+
icon.Icon = idleIcon;
|
|
103
|
+
icon.Text = "OwnRemote - running";
|
|
104
|
+
icon.ContextMenuStrip = menu;
|
|
105
|
+
icon.MouseDoubleClick += (s, e) => { if (e.Button == MouseButtons.Left) Emit("cmd open"); };
|
|
106
|
+
icon.Visible = true;
|
|
107
|
+
|
|
108
|
+
var reader = new Thread(ReadLoop);
|
|
109
|
+
reader.IsBackground = true;
|
|
110
|
+
reader.Start();
|
|
111
|
+
|
|
112
|
+
Emit("ready");
|
|
113
|
+
Application.Run();
|
|
114
|
+
icon.Visible = false;
|
|
115
|
+
icon.Dispose();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static void ReadLoop() {
|
|
119
|
+
string line;
|
|
120
|
+
while ((line = Console.In.ReadLine()) != null) {
|
|
121
|
+
string l = line;
|
|
122
|
+
try { ui.BeginInvoke((MethodInvoker)(() => Handle(l))); } catch { }
|
|
123
|
+
}
|
|
124
|
+
try { ui.BeginInvoke((MethodInvoker)(() => Quit())); } catch { }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static void Quit() {
|
|
128
|
+
icon.Visible = false;
|
|
129
|
+
Application.ExitThread();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static void Handle(string line) {
|
|
133
|
+
try {
|
|
134
|
+
int sp = line.IndexOf(' ');
|
|
135
|
+
string cmd = sp < 0 ? line : line.Substring(0, sp);
|
|
136
|
+
string[] f = (sp < 0 ? "" : line.Substring(sp + 1)).Split('\t');
|
|
137
|
+
switch (cmd) {
|
|
138
|
+
case "state": {
|
|
139
|
+
url = f[0];
|
|
140
|
+
code = f[1];
|
|
141
|
+
int online = int.Parse(f[2]);
|
|
142
|
+
urlItem.Text = url.Length > 0 ? "Open on phone: " + url + " (click to copy)" : "No network detected";
|
|
143
|
+
codeItem.Text = "Pairing code: " + code.Substring(0, 3) + " " + code.Substring(3) + " (click to copy)";
|
|
144
|
+
devicesItem.Text = "Devices: " + online + " online / " + f[3] + " paired";
|
|
145
|
+
pairItem.Text = f[4] == "1" ? "Lock pairing (reject new devices)" : "Unlock pairing";
|
|
146
|
+
publicUrl = f.Length > 5 ? f[5] : "";
|
|
147
|
+
publicItem.Visible = publicUrl.Length > 0;
|
|
148
|
+
publicItem.Text = "Internet (Cloudflare): " + publicUrl + " (click to copy)";
|
|
149
|
+
icon.Icon = online > 0 ? activeIcon : idleIcon;
|
|
150
|
+
string tip = "OwnRemote - running\n" + (online > 0 ? online + " device(s) connected" : "Code " + code.Substring(0, 3) + " " + code.Substring(3));
|
|
151
|
+
icon.Text = tip.Length > 63 ? tip.Substring(0, 63) : tip;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "notify":
|
|
155
|
+
icon.ShowBalloonTip(3000, f[0], f.Length > 1 && f[1].Length > 0 ? f[1] : " ", ToolTipIcon.Info);
|
|
156
|
+
break;
|
|
157
|
+
case "quit":
|
|
158
|
+
Quit();
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
} catch (Exception e) {
|
|
162
|
+
Console.Error.WriteLine(e.Message);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static void Copy(string text, string title) {
|
|
167
|
+
if (string.IsNullOrEmpty(text)) return;
|
|
168
|
+
try {
|
|
169
|
+
Clipboard.SetText(text);
|
|
170
|
+
icon.ShowBalloonTip(1500, title, text, ToolTipIcon.None);
|
|
171
|
+
} catch { }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
"@
|
|
175
|
+
|
|
176
|
+
if ($Render) {
|
|
177
|
+
# mode uji: simpan gambar ikon sebagai PNG
|
|
178
|
+
[OwnRemoteTray]::Draw($false, 64).Save("$Render-idle.png")
|
|
179
|
+
[OwnRemoteTray]::Draw($true, 64).Save("$Render-active.png")
|
|
180
|
+
exit 0
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
[OwnRemoteTray]::Run()
|
package/src/tray.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
|
|
5
|
+
// Ikon system tray (mode background) lewat proses PowerShell + WinForms NotifyIcon (tray-helper.ps1).
|
|
6
|
+
class TrayIcon {
|
|
7
|
+
constructor({ log = () => {}, onCommand = () => {} } = {}) {
|
|
8
|
+
this.log = log;
|
|
9
|
+
this.onCommand = onCommand;
|
|
10
|
+
this.proc = null;
|
|
11
|
+
this.lastState = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
start() {
|
|
15
|
+
if (this.proc) return;
|
|
16
|
+
const script = path.join(__dirname, 'tray-helper.ps1');
|
|
17
|
+
const proc = spawn(
|
|
18
|
+
'powershell.exe',
|
|
19
|
+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-STA', '-File', script],
|
|
20
|
+
{ windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
21
|
+
);
|
|
22
|
+
this.proc = proc;
|
|
23
|
+
|
|
24
|
+
let buf = '';
|
|
25
|
+
proc.stdout.on('data', (chunk) => {
|
|
26
|
+
buf += chunk;
|
|
27
|
+
let i;
|
|
28
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
29
|
+
const line = buf.slice(0, i).trim();
|
|
30
|
+
buf = buf.slice(i + 1);
|
|
31
|
+
if (line === 'ready') {
|
|
32
|
+
if (this.lastState) this.send(this.lastState);
|
|
33
|
+
} else if (line.startsWith('cmd ')) {
|
|
34
|
+
this.onCommand(line.slice(4));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
proc.stderr.on('data', (d) => {
|
|
39
|
+
const msg = String(d).trim();
|
|
40
|
+
if (msg) this.log(`Tray icon: ${msg}`);
|
|
41
|
+
});
|
|
42
|
+
proc.stdin.on('error', () => { /* proses sudah mati */ });
|
|
43
|
+
proc.on('error', (err) => this.log(`Tray icon failed to start: ${err.message}`));
|
|
44
|
+
proc.on('exit', () => {
|
|
45
|
+
if (this.proc === proc) this.proc = null;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
stop() {
|
|
50
|
+
if (!this.proc) return;
|
|
51
|
+
const p = this.proc;
|
|
52
|
+
this.proc = null;
|
|
53
|
+
try { p.stdin.end('quit\n'); } catch { /* abaikan */ }
|
|
54
|
+
setTimeout(() => { if (p.exitCode === null) p.kill(); }, 1500).unref();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
send(line) {
|
|
58
|
+
if (this.proc && this.proc.stdin.writable) this.proc.stdin.write(`${line}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
update({ url, pairCode, online, paired, allowPairing, publicUrl }) {
|
|
62
|
+
const clean = (s) => String(s).replace(/[\t\r\n]/g, ' ');
|
|
63
|
+
this.lastState = `state ${clean(url || '')}\t${pairCode}\t${online}\t${paired}\t${allowPairing ? 1 : 0}\t${clean(publicUrl || '')}`;
|
|
64
|
+
this.send(this.lastState);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
notify(title, text = '') {
|
|
68
|
+
const clean = (s) => String(s).replace(/[\t\r\n]/g, ' ');
|
|
69
|
+
this.send(`notify ${clean(title)}\t${clean(text)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Buka menu CLI di jendela console baru (dipanggil dari menu tray).
|
|
74
|
+
function openCliWindow() {
|
|
75
|
+
const cli = path.join(config.ROOT, 'cli.js');
|
|
76
|
+
const args = `/c start "OwnRemote" "${process.execPath}" "${cli}"`;
|
|
77
|
+
spawn('cmd.exe', [args], {
|
|
78
|
+
cwd: config.ROOT,
|
|
79
|
+
detached: true,
|
|
80
|
+
stdio: 'ignore',
|
|
81
|
+
windowsVerbatimArguments: true,
|
|
82
|
+
}).unref();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { TrayIcon, openCliWindow };
|
package/src/tunnel.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const { spawn, execFileSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Cloudflare Tunnel yang dijalankan & diawasi oleh OwnRemote.
|
|
6
|
+
// quick : `cloudflared tunnel --url http://localhost:<port>` -> URL acak *.trycloudflare.com (tanpa akun)
|
|
7
|
+
// token : `cloudflared tunnel run --token <token>` -> named tunnel dengan domain sendiri
|
|
8
|
+
// (public hostname diarahkan ke http://localhost:<port> di dashboard Cloudflare Zero Trust)
|
|
9
|
+
const MODES = ['off', 'quick', 'token'];
|
|
10
|
+
const QUICK_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
11
|
+
const CONNECTED_RE = /Registered tunnel connection|Connection [a-f0-9-]+ registered/i;
|
|
12
|
+
const RESTART_DELAYS = [2000, 5000, 15000, 30000];
|
|
13
|
+
|
|
14
|
+
function findCloudflared(customPath) {
|
|
15
|
+
const candidates = [
|
|
16
|
+
customPath,
|
|
17
|
+
path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'cloudflared', 'cloudflared.exe'),
|
|
18
|
+
path.join(process.env.ProgramFiles || 'C:\\Program Files', 'cloudflared', 'cloudflared.exe'),
|
|
19
|
+
].filter(Boolean);
|
|
20
|
+
for (const c of candidates) if (fs.existsSync(c)) return c;
|
|
21
|
+
try {
|
|
22
|
+
const found = execFileSync('where', ['cloudflared'], { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
23
|
+
.toString().split(/\r?\n/).find(Boolean);
|
|
24
|
+
if (found) return found.trim();
|
|
25
|
+
} catch { /* tidak ada di PATH */ }
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class TunnelManager {
|
|
30
|
+
constructor({ log = () => {}, onChange = () => {} } = {}) {
|
|
31
|
+
this.log = log;
|
|
32
|
+
this.onChange = onChange;
|
|
33
|
+
this.proc = null;
|
|
34
|
+
this.mode = 'off';
|
|
35
|
+
this.state = 'off'; // off | starting | connected | error
|
|
36
|
+
this.url = '';
|
|
37
|
+
this.error = '';
|
|
38
|
+
this.restarts = 0;
|
|
39
|
+
this.restartTimer = null;
|
|
40
|
+
this.opts = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
status() {
|
|
44
|
+
return { mode: this.mode, state: this.state, url: this.url, error: this.error };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Terapkan konfigurasi; aman dipanggil berulang (hanya restart bila ada yang berubah). */
|
|
48
|
+
apply({ mode, port, token, publicUrl, cloudflaredPath }) {
|
|
49
|
+
const next = { mode: MODES.includes(mode) ? mode : 'off', port, token: token || '', publicUrl: publicUrl || '', cloudflaredPath };
|
|
50
|
+
const same = this.opts && JSON.stringify(this.opts) === JSON.stringify(next);
|
|
51
|
+
if (same && (this.proc || next.mode === 'off')) return;
|
|
52
|
+
this.stop();
|
|
53
|
+
this.opts = next;
|
|
54
|
+
this.mode = next.mode;
|
|
55
|
+
this.restarts = 0;
|
|
56
|
+
if (next.mode === 'off') {
|
|
57
|
+
this.set('off', '', '');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.launch();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
launch() {
|
|
64
|
+
const { mode, port, token, publicUrl, cloudflaredPath } = this.opts;
|
|
65
|
+
const exe = findCloudflared(cloudflaredPath);
|
|
66
|
+
if (!exe) {
|
|
67
|
+
this.set('error', '', 'cloudflared not found. Install it with: winget install --id Cloudflare.cloudflared');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (mode === 'token' && !token) {
|
|
71
|
+
this.set('error', publicUrl, 'Named tunnel needs a tunnel token (Cloudflare Zero Trust > Networks > Tunnels).');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const args = mode === 'quick'
|
|
75
|
+
? ['tunnel', '--no-autoupdate', '--url', `http://localhost:${port}`]
|
|
76
|
+
: ['tunnel', '--no-autoupdate', 'run', '--token', token];
|
|
77
|
+
this.set('starting', mode === 'token' ? publicUrl : '', '');
|
|
78
|
+
this.log(mode === 'quick' ? 'Starting Cloudflare quick tunnel...' : 'Starting Cloudflare named tunnel...');
|
|
79
|
+
|
|
80
|
+
const proc = spawn(exe, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
81
|
+
this.proc = proc;
|
|
82
|
+
let tail = '';
|
|
83
|
+
const onOutput = (chunk) => {
|
|
84
|
+
const text = String(chunk);
|
|
85
|
+
tail = (tail + text).slice(-2000);
|
|
86
|
+
const m = mode === 'quick' && text.match(QUICK_URL_RE);
|
|
87
|
+
if (m && this.url !== m[0]) {
|
|
88
|
+
this.url = m[0];
|
|
89
|
+
this.onChange(this.status());
|
|
90
|
+
}
|
|
91
|
+
if (CONNECTED_RE.test(text) && this.state !== 'connected') {
|
|
92
|
+
this.restarts = 0;
|
|
93
|
+
this.set('connected', this.url || publicUrl, '');
|
|
94
|
+
this.log(`Cloudflare tunnel connected${this.url ? `: ${this.url}` : ''}.`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
proc.stdout.on('data', onOutput);
|
|
98
|
+
proc.stderr.on('data', onOutput);
|
|
99
|
+
proc.on('error', (err) => this.set('error', '', `Failed to run cloudflared: ${err.message}`));
|
|
100
|
+
proc.on('exit', (code) => {
|
|
101
|
+
if (this.proc !== proc) return; // dihentikan dengan sengaja
|
|
102
|
+
this.proc = null;
|
|
103
|
+
const reason = (tail.match(/ERR[^\n]*/g) || []).pop() || `cloudflared exited (code ${code})`;
|
|
104
|
+
const delay = RESTART_DELAYS[Math.min(this.restarts, RESTART_DELAYS.length - 1)];
|
|
105
|
+
this.restarts++;
|
|
106
|
+
this.set('error', '', `${reason.replace(/^.*?ERR\s*/, '').trim()} - retrying in ${delay / 1000}s`);
|
|
107
|
+
this.log(`Cloudflare tunnel stopped: ${reason.trim()}`);
|
|
108
|
+
this.restartTimer = setTimeout(() => this.launch(), delay);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
set(state, url, error) {
|
|
113
|
+
this.state = state;
|
|
114
|
+
this.url = url;
|
|
115
|
+
this.error = error;
|
|
116
|
+
this.onChange(this.status());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
stop() {
|
|
120
|
+
clearTimeout(this.restartTimer);
|
|
121
|
+
if (this.proc) {
|
|
122
|
+
const p = this.proc;
|
|
123
|
+
this.proc = null;
|
|
124
|
+
try { p.kill(); } catch { /* sudah mati */ }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { TunnelManager, findCloudflared, MODES };
|