Skip to content

Commit 1406405

Browse files
committed
feat: support service api on linux
1 parent 9a2de13 commit 1406405

File tree

1 file changed

+126
-52
lines changed

1 file changed

+126
-52
lines changed

src/service.ts

Lines changed: 126 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@ import { Connection } from "./types.js";
66

77
/**
88
* Cloudflared launchd identifier.
9+
* @platform macOS
910
*/
1011
export const identifier = "com.cloudflare.cloudflared";
1112

13+
/**
14+
* Cloudflared service name.
15+
* @platform linux
16+
*/
17+
export const service_name = "cloudflared.service";
18+
1219
/**
1320
* Path of service related files.
21+
* @platform macOS
1422
*/
1523
export const MACOS_SERVICE_PATH = {
1624
PLIST: is_root()
@@ -24,10 +32,21 @@ export const MACOS_SERVICE_PATH = {
2432
: `${os.homedir()}/Library/Logs/${identifier}.err.log`,
2533
};
2634

35+
/**
36+
* Path of service related files.
37+
* @platform linux
38+
*/
39+
export const LINUX_SERVICE_PATH = {
40+
SYSTEMD: `/etc/systemd/system/${service_name}`,
41+
SERVICE: "/etc/init.d/cloudflared",
42+
SERVICE_OUT: "/var/log/cloudflared.log",
43+
SERVICE_ERR: "/var/log/cloudflared.err",
44+
};
45+
2746
/**
2847
* Cloudflared Service API.
2948
*/
30-
export const service = { install, uninstall, exists, log, err, current, clean };
49+
export const service = { install, uninstall, exists, log, err, current, clean, journal };
3150

3251
/**
3352
* Throw when service is already installed.
@@ -50,9 +69,10 @@ export class NotInstalledError extends Error {
5069
/**
5170
* Install Cloudflared service.
5271
* @param token Tunnel service token.
72+
* @platform macOS, linux
5373
*/
5474
export function install(token?: string): void {
55-
if (process.platform !== "darwin") {
75+
if (!["darwin", "linux"].includes(process.platform)) {
5676
throw new Error(`Not Implemented on platform ${process.platform}`);
5777
}
5878

@@ -71,9 +91,10 @@ export function install(token?: string): void {
7191

7292
/**
7393
* Uninstall Cloudflared service.
94+
* @platform macOS, linux
7495
*/
7596
export function uninstall(): void {
76-
if (process.platform !== "darwin") {
97+
if (!["darwin", "linux"].includes(process.platform)) {
7798
throw new Error(`Not Implemented on platform ${process.platform}`);
7899
}
79100

@@ -83,45 +104,76 @@ export function uninstall(): void {
83104

84105
spawnSync(bin, ["service", "uninstall"]);
85106

86-
fs.rmSync(MACOS_SERVICE_PATH.OUT);
87-
fs.rmSync(MACOS_SERVICE_PATH.ERR);
107+
if (process.platform === "darwin") {
108+
fs.rmSync(MACOS_SERVICE_PATH.OUT);
109+
fs.rmSync(MACOS_SERVICE_PATH.ERR);
110+
} else if (process.platform === "linux" && !is_systemd()) {
111+
fs.rmSync(LINUX_SERVICE_PATH.SERVICE_OUT);
112+
fs.rmSync(LINUX_SERVICE_PATH.SERVICE_ERR);
113+
}
88114
}
89115

90116
/**
91117
* Get stdout log of cloudflared service. (Usually empty)
92118
* @returns stdout log of cloudflared service.
119+
* @platform macOS, linux (sysv)
93120
*/
94121
export function log(): string {
95-
if (process.platform !== "darwin") {
96-
throw new Error(`Not Implemented on platform ${process.platform}`);
97-
}
98-
99122
if (!exists()) {
100123
throw new NotInstalledError();
101124
}
102125

103-
return fs.readFileSync(MACOS_SERVICE_PATH.OUT, "utf8");
126+
if (process.platform === "darwin") {
127+
return fs.readFileSync(MACOS_SERVICE_PATH.OUT, "utf8");
128+
}
129+
130+
if (process.platform === "linux" && !is_systemd()) {
131+
return fs.readFileSync(LINUX_SERVICE_PATH.SERVICE_OUT, "utf8");
132+
}
133+
134+
throw new Error(`Not Implemented on platform ${process.platform}`);
104135
}
105136

106137
/**
107138
* Get stderr log of cloudflared service. (cloudflared print all things here)
108139
* @returns stderr log of cloudflared service.
140+
* @platform macOS, linux (sysv)
109141
*/
110142
export function err(): string {
111-
if (process.platform !== "darwin") {
112-
throw new Error(`Not Implemented on platform ${process.platform}`);
113-
}
114-
115143
if (!exists()) {
116144
throw new NotInstalledError();
117145
}
118146

119-
return fs.readFileSync(MACOS_SERVICE_PATH.ERR, "utf8");
147+
if (process.platform === "darwin") {
148+
return fs.readFileSync(MACOS_SERVICE_PATH.ERR, "utf8");
149+
}
150+
151+
if (process.platform === "linux" && !is_systemd()) {
152+
return fs.readFileSync(LINUX_SERVICE_PATH.SERVICE_ERR, "utf8");
153+
}
154+
155+
throw new Error(`Not Implemented on platform ${process.platform}`);
156+
}
157+
158+
/**
159+
* Get cloudflared service journal from journalctl.
160+
* @param n The number of entries to return.
161+
* @returns cloudflared service journal.
162+
* @platform linux (systemd)
163+
*/
164+
export function journal(n = 300): string {
165+
if (process.platform === "linux" && is_systemd()) {
166+
const args = ["-u", service_name, "-o", "cat", "-n", n.toString()];
167+
return spawnSync("journalctl", args).stdout.toString();
168+
}
169+
170+
throw new Error(`Not Implemented on platform ${process.platform}`);
120171
}
121172

122173
/**
123174
* Get informations of current running cloudflared service.
124175
* @returns informations of current running cloudflared service.
176+
* @platform macOS, linux
125177
*/
126178
export function current(): {
127179
/** Tunnel ID */
@@ -134,61 +186,72 @@ export function current(): {
134186
metrics: string;
135187
/** Tunnel Configuration */
136188
config: {
137-
ingress: { service: string; hostname?: string }[];
189+
ingress?: { service: string; hostname?: string }[];
138190
[key: string]: unknown;
139191
};
140192
} {
141-
if (process.platform !== "darwin") {
193+
if (!["darwin", "linux"].includes(process.platform)) {
142194
throw new Error(`Not Implemented on platform ${process.platform}`);
143195
}
144196

145197
if (!exists()) {
146198
throw new NotInstalledError();
147199
}
148200

149-
const error = err();
201+
const log = is_systemd() ? journal() : err();
150202

151203
const regex = {
152-
tunnelID: /tunnelID=([0-9a-z-]+)/g,
153-
connectorID: /Connector ID: ([0-9a-z-]+)/g,
154-
connections:
155-
/Connection ([a-z0-9-]+) registered connIndex=(\d) ip=([0-9.]+) location=([A-Z]+)/g,
156-
metrics: /metrics server on ([0-9.:]+\/metrics)/g,
157-
config: /config="(.+[^\\])"/g,
204+
tunnelID: /tunnelID=([0-9a-z-]+)/,
205+
connectorID: /Connector ID: ([0-9a-z-]+)/,
206+
connect: /Connection ([a-z0-9-]+) registered connIndex=(\d) ip=([0-9.]+) location=([A-Z]+)/,
207+
disconnect: /Unregistered tunnel connection connIndex=(\d)/,
208+
metrics: /metrics server on ([0-9.:]+\/metrics)/,
209+
config: /config="(.+[^\\])"/,
158210
};
159211

160-
const match = {
161-
tunnelID: regex.tunnelID.exec(error),
162-
connectorID: regex.connectorID.exec(error),
163-
connections: error.matchAll(regex.connections),
164-
metrics: regex.metrics.exec(error),
165-
config: regex.config.exec(error),
166-
};
212+
let tunnelID = "";
213+
let connectorID = "";
214+
const connections: Connection[] = [];
215+
let metrics = "";
216+
let config: {
217+
ingress?: { service: string; hostname?: string }[];
218+
[key: string]: unknown;
219+
} = {};
167220

168-
const config = (() => {
221+
for (const line of log.split("\n")) {
169222
try {
170-
return JSON.parse(match.config?.[1].replace(/\\/g, "") ?? "{}");
171-
} catch (e) {
172-
return {};
223+
if (line.match(regex.tunnelID)) {
224+
tunnelID = line.match(regex.tunnelID)?.[1] ?? "";
225+
} else if (line.match(regex.connectorID)) {
226+
connectorID = line.match(regex.connectorID)?.[1] ?? "";
227+
} else if (line.match(regex.connect)) {
228+
const [, id, idx, ip, location] = line.match(regex.connect) ?? [];
229+
if (id && idx && ip && location) {
230+
connections[parseInt(idx)] = { id, ip, location };
231+
}
232+
} else if (line.match(regex.disconnect)) {
233+
const [, idx] = line.match(regex.disconnect) ?? [];
234+
if (parseInt(idx) in connections) {
235+
connections[parseInt(idx)] = { id: "", ip: "", location: "" };
236+
}
237+
} else if (line.match(regex.metrics)) {
238+
metrics = line.match(regex.metrics)?.[1] ?? "";
239+
} else if (line.match(regex.config)) {
240+
config = JSON.parse(line.match(regex.config)?.[1].replace(/\\/g, "") ?? "{}");
241+
}
242+
} catch (err) {
243+
if (process.env.VERBOSE) {
244+
console.error("log parsing failed", err);
245+
}
173246
}
174-
})();
175-
176-
return {
177-
tunnelID: match.tunnelID?.[1] ?? "",
178-
connectorID: match.connectorID?.[1] ?? "",
179-
connections:
180-
[...match.connections].map(([, id, , ip, location]) => ({
181-
id,
182-
ip,
183-
location,
184-
})) ?? [],
185-
metrics: match.metrics?.[1] ?? "",
186-
config,
187-
};
247+
}
248+
249+
return { tunnelID, connectorID, connections, metrics, config };
188250
}
189251

190252
/**
191253
* Clean up service log files.
254+
* @platform macOS
192255
*/
193256
export function clean(): void {
194257
if (process.platform !== "darwin") {
@@ -206,13 +269,24 @@ export function clean(): void {
206269
/**
207270
* Check if cloudflared service is installed.
208271
* @returns true if service is installed, false otherwise.
272+
* @platform macOS, linux
209273
*/
210274
export function exists(): boolean {
211-
return is_root()
212-
? fs.existsSync(MACOS_SERVICE_PATH.PLIST)
213-
: fs.existsSync(MACOS_SERVICE_PATH.PLIST);
275+
if (process.platform === "darwin") {
276+
return fs.existsSync(MACOS_SERVICE_PATH.PLIST);
277+
} else if (process.platform === "linux") {
278+
return is_systemd()
279+
? fs.existsSync(LINUX_SERVICE_PATH.SYSTEMD)
280+
: fs.existsSync(LINUX_SERVICE_PATH.SERVICE);
281+
}
282+
283+
throw new Error(`Not Implemented on platform ${process.platform}`);
214284
}
215285

216286
function is_root(): boolean {
217287
return process.getuid?.() === 0;
218288
}
289+
290+
function is_systemd(): boolean {
291+
return process.platform === "linux" && fs.existsSync("/run/systemd/system");
292+
}

0 commit comments

Comments
 (0)