@bitsocial/bitsocial-cli 0.19.39
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 +674 -0
- package/README.md +706 -0
- package/bin/dev +20 -0
- package/bin/dev.cmd +3 -0
- package/bin/postinstall.js +125 -0
- package/bin/run +13 -0
- package/bin/run.cmd +3 -0
- package/dist/challenge-packages/challenge-utils.d.ts +24 -0
- package/dist/challenge-packages/challenge-utils.js +304 -0
- package/dist/cli/base-command.d.ts +11 -0
- package/dist/cli/base-command.js +45 -0
- package/dist/cli/commands/challenge/install.d.ts +12 -0
- package/dist/cli/commands/challenge/install.js +131 -0
- package/dist/cli/commands/challenge/list.d.ts +10 -0
- package/dist/cli/commands/challenge/list.js +37 -0
- package/dist/cli/commands/challenge/remove.d.ts +12 -0
- package/dist/cli/commands/challenge/remove.js +60 -0
- package/dist/cli/commands/community/create.d.ts +12 -0
- package/dist/cli/commands/community/create.js +54 -0
- package/dist/cli/commands/community/delete.d.ts +10 -0
- package/dist/cli/commands/community/delete.js +44 -0
- package/dist/cli/commands/community/edit.d.ts +12 -0
- package/dist/cli/commands/community/edit.js +74 -0
- package/dist/cli/commands/community/get.d.ts +9 -0
- package/dist/cli/commands/community/get.js +32 -0
- package/dist/cli/commands/community/list.d.ts +9 -0
- package/dist/cli/commands/community/list.js +30 -0
- package/dist/cli/commands/community/start.d.ts +13 -0
- package/dist/cli/commands/community/start.js +46 -0
- package/dist/cli/commands/community/stop.d.ts +10 -0
- package/dist/cli/commands/community/stop.js +44 -0
- package/dist/cli/commands/daemon.d.ts +14 -0
- package/dist/cli/commands/daemon.js +484 -0
- package/dist/cli/commands/logs.d.ts +24 -0
- package/dist/cli/commands/logs.js +199 -0
- package/dist/cli/commands/subplebbit/create.d.ts +12 -0
- package/dist/cli/commands/subplebbit/create.js +54 -0
- package/dist/cli/commands/subplebbit/edit.d.ts +12 -0
- package/dist/cli/commands/subplebbit/edit.js +73 -0
- package/dist/cli/commands/subplebbit/get.d.ts +9 -0
- package/dist/cli/commands/subplebbit/get.js +32 -0
- package/dist/cli/commands/subplebbit/list.d.ts +9 -0
- package/dist/cli/commands/subplebbit/list.js +30 -0
- package/dist/cli/commands/subplebbit/start.d.ts +10 -0
- package/dist/cli/commands/subplebbit/start.js +41 -0
- package/dist/cli/commands/subplebbit/stop.d.ts +10 -0
- package/dist/cli/commands/subplebbit/stop.js +43 -0
- package/dist/cli/commands/update/check.d.ts +6 -0
- package/dist/cli/commands/update/check.js +28 -0
- package/dist/cli/commands/update/install.d.ts +12 -0
- package/dist/cli/commands/update/install.js +63 -0
- package/dist/cli/commands/update/versions.d.ts +9 -0
- package/dist/cli/commands/update/versions.js +29 -0
- package/dist/cli/hooks/init/version-hook.d.ts +3 -0
- package/dist/cli/hooks/init/version-hook.js +43 -0
- package/dist/cli/hooks/prerun/parse-dynamic-flags-hook.d.ts +3 -0
- package/dist/cli/hooks/prerun/parse-dynamic-flags-hook.js +94 -0
- package/dist/cli/types.d.ts +4 -0
- package/dist/cli/types.js +1 -0
- package/dist/common-utils/data-migration.d.ts +1 -0
- package/dist/common-utils/data-migration.js +27 -0
- package/dist/common-utils/defaults.d.ts +9 -0
- package/dist/common-utils/defaults.js +10 -0
- package/dist/common-utils/resolvers.d.ts +2 -0
- package/dist/common-utils/resolvers.js +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ipfs/startIpfs.d.ts +3 -0
- package/dist/ipfs/startIpfs.js +304 -0
- package/dist/seeder.d.ts +1 -0
- package/dist/seeder.js +83 -0
- package/dist/update/npm-registry.d.ts +6 -0
- package/dist/update/npm-registry.js +66 -0
- package/dist/update/semver.d.ts +5 -0
- package/dist/update/semver.js +29 -0
- package/dist/util.d.ts +31 -0
- package/dist/util.js +157 -0
- package/dist/webui/daemon-server.d.ts +10 -0
- package/dist/webui/daemon-server.js +140 -0
- package/package.json +143 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getPKCLogger } from "../../../util.js";
|
|
2
|
+
import { BaseCommand } from "../../base-command.js";
|
|
3
|
+
import { Args } from "@oclif/core";
|
|
4
|
+
export default class Stop extends BaseCommand {
|
|
5
|
+
static description = "Stop a community. The community will not publish or receive any publications until it is started again.";
|
|
6
|
+
static strict = false; // To allow for variable length arguments
|
|
7
|
+
static args = {
|
|
8
|
+
addresses: Args.string({
|
|
9
|
+
name: "addresses",
|
|
10
|
+
required: true,
|
|
11
|
+
description: "Addresses of communities to stop. Separated by space"
|
|
12
|
+
})
|
|
13
|
+
};
|
|
14
|
+
static examples = [
|
|
15
|
+
"bitsocial community stop plebbit.bso",
|
|
16
|
+
"bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW"
|
|
17
|
+
];
|
|
18
|
+
async run() {
|
|
19
|
+
const { argv, flags } = await this.parse(Stop);
|
|
20
|
+
const log = (await getPKCLogger())("bitsocial-cli:commands:community:stop");
|
|
21
|
+
log(`addresses: `, argv);
|
|
22
|
+
log(`flags: `, flags);
|
|
23
|
+
const addresses = argv;
|
|
24
|
+
if (!Array.isArray(addresses))
|
|
25
|
+
this.error(`Failed to parse addresses correctly (${addresses})`);
|
|
26
|
+
const pkc = await this._connectToPkcRpc(flags.pkcRpcUrl.toString());
|
|
27
|
+
for (const address of addresses) {
|
|
28
|
+
try {
|
|
29
|
+
const community = await pkc.createCommunity({ address });
|
|
30
|
+
await community.stop();
|
|
31
|
+
this.log(address);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
const error = e instanceof Error ? e : new Error(typeof e === "string" ? e : JSON.stringify(e));
|
|
35
|
+
//@ts-expect-error
|
|
36
|
+
error.details = { ...error.details, address };
|
|
37
|
+
console.error(error);
|
|
38
|
+
await pkc.destroy();
|
|
39
|
+
this.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
await pkc.destroy();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
export default class Daemon extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
pkcRpcUrl: import("@oclif/core/interfaces").OptionFlag<import("url").URL, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
logPath: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
chainProviderUrls: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
static examples: string[];
|
|
10
|
+
private _setupLogger;
|
|
11
|
+
private _getNewLogfileByEvacuatingOldLogsIfNeeded;
|
|
12
|
+
private _pipeDebugLogsToLogFile;
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { Flags, Command } from "@oclif/core";
|
|
2
|
+
import defaults from "../../common-utils/defaults.js";
|
|
3
|
+
import { startKuboNode } from "../../ipfs/startIpfs.js";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import tcpPortUsed from "tcp-port-used";
|
|
6
|
+
import { getLanIpV4Address, getPKCLogger, setupDebugLogger, loadKuboConfigFile, parseMultiAddrKuboRpcToUrl, parseMultiAddrIpfsGatewayToUrl } from "../../util.js";
|
|
7
|
+
import { startDaemonServer } from "../../webui/daemon-server.js";
|
|
8
|
+
import { loadChallengesIntoPKC } from "../../challenge-packages/challenge-utils.js";
|
|
9
|
+
import { migrateDataDirectory } from "../../common-utils/data-migration.js";
|
|
10
|
+
import { createBsoResolvers } from "../../common-utils/resolvers.js";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import fsPromise from "fs/promises";
|
|
13
|
+
/** Replace wildcard bind addresses with loopback for connectivity checks (macOS rejects connect to 0.0.0.0 with EINVAL) */
|
|
14
|
+
function toConnectableHostname(hostname) {
|
|
15
|
+
if (process.platform === "darwin") {
|
|
16
|
+
if (hostname === "0.0.0.0")
|
|
17
|
+
return "127.0.0.1";
|
|
18
|
+
if (hostname === "::")
|
|
19
|
+
return "::1";
|
|
20
|
+
}
|
|
21
|
+
return hostname;
|
|
22
|
+
}
|
|
23
|
+
import { EOL } from "node:os";
|
|
24
|
+
import { formatWithOptions } from "node:util";
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
//@ts-expect-error
|
|
27
|
+
import DataObjectParser from "dataobject-parser";
|
|
28
|
+
import * as remeda from "remeda";
|
|
29
|
+
const defaultPkcOptions = {
|
|
30
|
+
dataPath: defaults.PKC_DATA_PATH,
|
|
31
|
+
httpRoutersOptions: defaults.HTTP_TRACKERS
|
|
32
|
+
};
|
|
33
|
+
export default class Daemon extends Command {
|
|
34
|
+
static description = `Run a network-connected Bitsocial node. Once the daemon is running you can create and start your communities and receive publications from users. The daemon will also serve web ui on http that can be accessed through a browser on any machine. Within the web ui users are able to browse, create and manage their communities fully P2P.
|
|
35
|
+
Options can be passed to the RPC's instance through flag --pkcOptions.optionName. For a list of pkc options (https://github.com/pkcprotocol/pkc-js?tab=readme-ov-file#pkcoptions)
|
|
36
|
+
If you need to modify ipfs config, you should head to {bitsocial-data-path}/.ipfs-bitsocial-cli/config and modify the config file
|
|
37
|
+
`;
|
|
38
|
+
static flags = {
|
|
39
|
+
pkcRpcUrl: Flags.url({
|
|
40
|
+
description: "Specify PKC RPC URL to listen on",
|
|
41
|
+
required: true,
|
|
42
|
+
default: defaults.PKC_RPC_URL
|
|
43
|
+
}),
|
|
44
|
+
logPath: Flags.directory({
|
|
45
|
+
description: "Specify a directory which will be used to store logs",
|
|
46
|
+
required: true,
|
|
47
|
+
default: defaults.PKC_LOG_PATH
|
|
48
|
+
}),
|
|
49
|
+
chainProviderUrls: Flags.string({
|
|
50
|
+
description: 'Ethereum RPC URL(s) for .bso/.eth name resolution. Can be specified multiple times. Defaults to viem public transport and https://ethrpc.xyz',
|
|
51
|
+
multiple: true,
|
|
52
|
+
default: ["viem", "https://ethrpc.xyz"]
|
|
53
|
+
})
|
|
54
|
+
};
|
|
55
|
+
static examples = [
|
|
56
|
+
"bitsocial daemon",
|
|
57
|
+
"bitsocial daemon --pkcRpcUrl ws://localhost:53812",
|
|
58
|
+
"bitsocial daemon --pkcOptions.dataPath /tmp/bitsocial-datapath/",
|
|
59
|
+
"bitsocial daemon --pkcOptions.kuboRpcClientsOptions[0] https://remoteipfsnode.com",
|
|
60
|
+
"bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY",
|
|
61
|
+
"bitsocial daemon --chainProviderUrls viem --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY"
|
|
62
|
+
];
|
|
63
|
+
_setupLogger(Logger) {
|
|
64
|
+
setupDebugLogger(Logger, { enableDefaultNamespace: true });
|
|
65
|
+
console.log("To view logs, run: bitsocial logs");
|
|
66
|
+
console.log("For custom debug logging, restart the daemon with DEBUG env, e.g.: DEBUG='bitsocial*,pkc*' bitsocial daemon");
|
|
67
|
+
}
|
|
68
|
+
async _getNewLogfileByEvacuatingOldLogsIfNeeded(logPath) {
|
|
69
|
+
try {
|
|
70
|
+
await fsPromise.mkdir(logPath, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
//@ts-expect-error
|
|
74
|
+
if (e.code !== "EEXIST")
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
const logFiles = (await fsPromise.readdir(logPath, { withFileTypes: true })).filter((file) => file.name.startsWith("bitsocial_cli_daemon"));
|
|
78
|
+
const logfilesCapacity = 5; // we only store 5 log files
|
|
79
|
+
let deletedLogFile;
|
|
80
|
+
if (logFiles.length >= logfilesCapacity) {
|
|
81
|
+
// we need to pick the oldest log to delete
|
|
82
|
+
const logFileToDelete = logFiles.map((logFile) => logFile.name).sort()[0]; // TODO need to test this, not sure if it works
|
|
83
|
+
deletedLogFile = logFileToDelete;
|
|
84
|
+
await fsPromise.rm(path.join(logPath, logFileToDelete));
|
|
85
|
+
}
|
|
86
|
+
return { logFilePath: path.join(logPath, `bitsocial_cli_daemon_${new Date().toISOString().replace(/:/g, "-")}.log`), deletedLogFile, logfilesCapacity };
|
|
87
|
+
}
|
|
88
|
+
async _pipeDebugLogsToLogFile(logPath, Logger) {
|
|
89
|
+
const { logFilePath, deletedLogFile, logfilesCapacity } = await this._getNewLogfileByEvacuatingOldLogsIfNeeded(logPath);
|
|
90
|
+
const logFile = fs.createWriteStream(logFilePath, { flags: "a" });
|
|
91
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
92
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
93
|
+
const isLogFileOverLimit = () => logFile.bytesWritten > 20000000; // 20mb
|
|
94
|
+
const writeTimestampedLine = (text) => {
|
|
95
|
+
if (isLogFileOverLimit())
|
|
96
|
+
return;
|
|
97
|
+
if (!text || text.trim().length === 0)
|
|
98
|
+
return;
|
|
99
|
+
const timestamp = `[${new Date().toISOString()}] `;
|
|
100
|
+
const lines = text.split("\n");
|
|
101
|
+
const timestamped = lines.map((line, i) => (i === 0 ? timestamp + line : line)).join("\n");
|
|
102
|
+
logFile.write(timestamped);
|
|
103
|
+
};
|
|
104
|
+
// Redirect debug library output directly to the log file
|
|
105
|
+
// instead of stderr, so only real errors appear in the terminal
|
|
106
|
+
const require = createRequire(import.meta.url);
|
|
107
|
+
const debugModule = require("debug");
|
|
108
|
+
// Force colors on and suppress the debug library's own date prefix
|
|
109
|
+
// so that only writeTimestampedLine adds timestamps
|
|
110
|
+
debugModule.inspectOpts.colors = true;
|
|
111
|
+
debugModule.inspectOpts.hideDate = true;
|
|
112
|
+
debugModule.log = (...args) => {
|
|
113
|
+
writeTimestampedLine(formatWithOptions({ depth: Logger.inspectOpts?.depth || 10, colors: true }, ...args).trimStart() + EOL);
|
|
114
|
+
};
|
|
115
|
+
const asString = (data) => (typeof data === "string" ? data : Buffer.from(data).toString());
|
|
116
|
+
process.stdout.write = (...args) => {
|
|
117
|
+
//@ts-expect-error
|
|
118
|
+
const res = stdoutWrite(...args);
|
|
119
|
+
writeTimestampedLine(asString(args[0]));
|
|
120
|
+
return res;
|
|
121
|
+
};
|
|
122
|
+
process.stderr.write = (...args) => {
|
|
123
|
+
// Only write stderr to the log file, not to the terminal.
|
|
124
|
+
// Debug output goes to stderr; we want it in logs only.
|
|
125
|
+
// Real errors are caught by uncaughtException/unhandledRejection handlers
|
|
126
|
+
// which use console.error -> stderr.write -> this override -> log file.
|
|
127
|
+
writeTimestampedLine(asString(args[0]).trimStart());
|
|
128
|
+
return true;
|
|
129
|
+
};
|
|
130
|
+
const log = Logger("bitsocial-cli:daemon");
|
|
131
|
+
log(`Will store stderr + stdout log to ${logFilePath}`);
|
|
132
|
+
if (deletedLogFile) {
|
|
133
|
+
log(`Will remove log (${deletedLogFile}) because we reached capacity (${logfilesCapacity})`);
|
|
134
|
+
}
|
|
135
|
+
// Write real errors to both the terminal and the log file
|
|
136
|
+
const writeErrorToTerminal = (err) => {
|
|
137
|
+
const msg = err instanceof Error ? err.stack || err.message : String(err);
|
|
138
|
+
stderrWrite(msg + EOL);
|
|
139
|
+
};
|
|
140
|
+
process.on("uncaughtException", (err) => {
|
|
141
|
+
writeErrorToTerminal(err);
|
|
142
|
+
console.error(err);
|
|
143
|
+
});
|
|
144
|
+
process.on("unhandledRejection", (err) => {
|
|
145
|
+
writeErrorToTerminal(err);
|
|
146
|
+
console.error(err);
|
|
147
|
+
});
|
|
148
|
+
process.on("exit", () => logFile.close());
|
|
149
|
+
return { logFilePath, stdoutWrite };
|
|
150
|
+
}
|
|
151
|
+
async run() {
|
|
152
|
+
// Non-blocking update check — fire-and-forget, won't delay startup
|
|
153
|
+
import("../../update/npm-registry.js")
|
|
154
|
+
.then(({ fetchLatestVersion }) => fetchLatestVersion().then(async (latest) => {
|
|
155
|
+
const { compareVersions } = await import("../../update/semver.js");
|
|
156
|
+
if (compareVersions(latest, this.config.version) > 0) {
|
|
157
|
+
this.log(`Update available: v${latest} (current: v${this.config.version}). Run 'bitsocial update install' to upgrade.`);
|
|
158
|
+
}
|
|
159
|
+
}))
|
|
160
|
+
.catch(() => { }); // silently ignore errors (offline, npm unavailable, etc.)
|
|
161
|
+
process.env["DEBUG_COLORS"] = "1";
|
|
162
|
+
process.env["DEBUG_HIDE_DATE"] = "1";
|
|
163
|
+
const { flags } = await this.parse(Daemon);
|
|
164
|
+
const Logger = await getPKCLogger();
|
|
165
|
+
this._setupLogger(Logger);
|
|
166
|
+
const { logFilePath, stdoutWrite } = await this._pipeDebugLogsToLogFile(flags.logPath, Logger);
|
|
167
|
+
const log = Logger("bitsocial-cli:daemon");
|
|
168
|
+
try {
|
|
169
|
+
// Log debug info after pipe is set up so it goes to the log file, not terminal
|
|
170
|
+
const envDebug = process.env["_PKC_DEBUG"] || process.env["DEBUG"];
|
|
171
|
+
const debugNamespace = envDebug === "0" || envDebug === "" ? undefined : envDebug;
|
|
172
|
+
if (debugNamespace) {
|
|
173
|
+
const debugDepth = process.env["DEBUG_DEPTH"] ? parseInt(process.env["DEBUG_DEPTH"]) : 10;
|
|
174
|
+
log("Debug logs is on with namespace", `"${debugNamespace}"`);
|
|
175
|
+
log("Debug depth is set to", debugDepth);
|
|
176
|
+
}
|
|
177
|
+
log(`flags: `, flags);
|
|
178
|
+
const pkcRpcUrl = new URL(flags.pkcRpcUrl);
|
|
179
|
+
const pkcOptionsFlagNames = Object.keys(flags).filter((flag) => flag.startsWith("pkcOptions"));
|
|
180
|
+
const pkcOptionsFromFlag = pkcOptionsFlagNames.length > 0
|
|
181
|
+
? DataObjectParser.transpose(remeda.pick(flags, pkcOptionsFlagNames))["_data"]?.["pkcOptions"]
|
|
182
|
+
: undefined;
|
|
183
|
+
if (pkcOptionsFromFlag?.pkcRpcClientsOptions && pkcRpcUrl.toString() !== defaults.PKC_RPC_URL.toString()) {
|
|
184
|
+
this.error("Can't provide pkcOptions.pkcRpcClientsOptions and --pkcRpcUrl simultaneously. You have to choose between connecting to an RPC or starting up a new RPC");
|
|
185
|
+
}
|
|
186
|
+
if (pkcOptionsFromFlag?.kuboRpcClientsOptions && pkcOptionsFromFlag.kuboRpcClientsOptions.length !== 1)
|
|
187
|
+
this.error("Can't provide pkcOptions.kuboRpcClientsOptions as an array with more than 1 element, or as a non array");
|
|
188
|
+
if (pkcOptionsFromFlag?.ipfsGatewayUrls && pkcOptionsFromFlag.ipfsGatewayUrls.length !== 1)
|
|
189
|
+
this.error("Can't provide pkcOptions.ipfsGatewayUrls as an array with more than 1 element, or as a non array");
|
|
190
|
+
const ipfsConfig = await loadKuboConfigFile(pkcOptionsFromFlag?.dataPath || defaultPkcOptions.dataPath);
|
|
191
|
+
const kuboRpcEndpoint = pkcOptionsFromFlag?.kuboRpcClientsOptions
|
|
192
|
+
? new URL(pkcOptionsFromFlag.kuboRpcClientsOptions[0].toString())
|
|
193
|
+
: ipfsConfig?.["Addresses"]?.["API"]
|
|
194
|
+
? await parseMultiAddrKuboRpcToUrl(ipfsConfig?.["Addresses"]?.["API"])
|
|
195
|
+
: defaults.KUBO_RPC_URL;
|
|
196
|
+
const ipfsGatewayEndpoint = pkcOptionsFromFlag?.ipfsGatewayUrls
|
|
197
|
+
? new URL(pkcOptionsFromFlag.ipfsGatewayUrls[0])
|
|
198
|
+
: ipfsConfig?.["Addresses"]?.["Gateway"]
|
|
199
|
+
? await parseMultiAddrIpfsGatewayToUrl(ipfsConfig?.["Addresses"]?.["Gateway"])
|
|
200
|
+
: defaults.IPFS_GATEWAY_URL;
|
|
201
|
+
defaultPkcOptions.kuboRpcClientsOptions = [kuboRpcEndpoint.toString()];
|
|
202
|
+
const mergedPkcOptions = { ...defaultPkcOptions, ...pkcOptionsFromFlag };
|
|
203
|
+
log("Merged pkc options that will be used for this node", mergedPkcOptions);
|
|
204
|
+
// Migrate data directory before creating PKC instance
|
|
205
|
+
migrateDataDirectory(mergedPkcOptions.dataPath);
|
|
206
|
+
// Create BSO name resolvers for .bso/.eth domain resolution
|
|
207
|
+
const bsoResolvers = createBsoResolvers(flags.chainProviderUrls, mergedPkcOptions.dataPath);
|
|
208
|
+
mergedPkcOptions.nameResolvers = [...(mergedPkcOptions.nameResolvers || []), ...bsoResolvers];
|
|
209
|
+
let mainProcessExited = false;
|
|
210
|
+
let pendingKuboStart;
|
|
211
|
+
// Kubo Node may fail randomly, we need to set a listener so when it exits because of an error we restart it
|
|
212
|
+
let kuboProcess;
|
|
213
|
+
const keepKuboUp = async () => {
|
|
214
|
+
if (mainProcessExited)
|
|
215
|
+
return;
|
|
216
|
+
const kuboApiPort = Number(kuboRpcEndpoint.port);
|
|
217
|
+
if (kuboProcess || pendingKuboStart || usingDifferentProcessRpc)
|
|
218
|
+
return; // already started, no need to intervene
|
|
219
|
+
const connectHostname = toConnectableHostname(kuboRpcEndpoint.hostname);
|
|
220
|
+
const isKuboApiPortTaken = await tcpPortUsed.check(kuboApiPort, connectHostname);
|
|
221
|
+
if (isKuboApiPortTaken) {
|
|
222
|
+
const connectableEndpoint = new URL(kuboRpcEndpoint.toString());
|
|
223
|
+
connectableEndpoint.hostname = connectHostname;
|
|
224
|
+
const versionUrl = new URL("version", connectableEndpoint);
|
|
225
|
+
const controller = new AbortController();
|
|
226
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
227
|
+
let isHealthyKubo = false;
|
|
228
|
+
try {
|
|
229
|
+
const response = await fetch(versionUrl, { method: "POST", signal: controller.signal });
|
|
230
|
+
isHealthyKubo = response.ok;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
/* ignore */
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
}
|
|
238
|
+
if (isHealthyKubo) {
|
|
239
|
+
log.trace(`Kubo API already running on port (${kuboApiPort}) by another program. bitsocial-cli will use the running ipfs daemon instead of starting a new one`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
throw new Error(`Cannot start IPFS daemon because the IPFS API port ${kuboRpcEndpoint.hostname}:${kuboApiPort} (configured as ${kuboRpcEndpoint.toString()}) is already in use.`);
|
|
243
|
+
}
|
|
244
|
+
const startPromise = startKuboNode(kuboRpcEndpoint, ipfsGatewayEndpoint, mergedPkcOptions.dataPath, (process) => {
|
|
245
|
+
kuboProcess = process;
|
|
246
|
+
});
|
|
247
|
+
pendingKuboStart = startPromise;
|
|
248
|
+
let startedProcess;
|
|
249
|
+
try {
|
|
250
|
+
startedProcess = await startPromise;
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
pendingKuboStart = undefined;
|
|
254
|
+
if (!mainProcessExited)
|
|
255
|
+
kuboProcess = undefined;
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
pendingKuboStart = undefined;
|
|
259
|
+
if (mainProcessExited) {
|
|
260
|
+
if (startedProcess?.pid && !startedProcess.killed) {
|
|
261
|
+
// Race condition: Kubo finished starting after mainProcessExited.
|
|
262
|
+
// Use SIGKILL + process group kill for immediate termination.
|
|
263
|
+
const pid = startedProcess.pid;
|
|
264
|
+
if (process.platform !== "win32") {
|
|
265
|
+
try {
|
|
266
|
+
process.kill(-pid, "SIGKILL");
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
/* best effort */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
process.kill(pid, "SIGKILL");
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
/* best effort */
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
kuboProcess = undefined;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
kuboProcess = startedProcess;
|
|
283
|
+
log(`Started kubo ipfs process with pid (${kuboProcess.pid})`);
|
|
284
|
+
console.log(`Kubo IPFS API listening on: ${kuboRpcEndpoint}`);
|
|
285
|
+
console.log(`Kubo IPFS Gateway listening on: ${ipfsGatewayEndpoint}`);
|
|
286
|
+
const currentProcess = startedProcess;
|
|
287
|
+
const onKuboExit = async () => {
|
|
288
|
+
// Restart Kubo process because it failed
|
|
289
|
+
if (!mainProcessExited) {
|
|
290
|
+
log(`Kubo node with pid (${currentProcess?.pid}) exited. Will attempt to restart it`);
|
|
291
|
+
kuboProcess = undefined;
|
|
292
|
+
try {
|
|
293
|
+
await keepKuboUp();
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
log.trace(`keepKuboUp error after kubo exit (interval will retry): ${error instanceof Error ? error.message : String(error)}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
currentProcess.removeAllListeners();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
currentProcess.once("exit", onKuboExit);
|
|
304
|
+
};
|
|
305
|
+
let startedOwnRpc = false;
|
|
306
|
+
let usingDifferentProcessRpc = false;
|
|
307
|
+
let daemonServer;
|
|
308
|
+
const createOrConnectRpc = async () => {
|
|
309
|
+
if (mainProcessExited)
|
|
310
|
+
return;
|
|
311
|
+
if (startedOwnRpc)
|
|
312
|
+
return;
|
|
313
|
+
const isRpcPortTaken = await tcpPortUsed.check(Number(pkcRpcUrl.port), pkcRpcUrl.hostname);
|
|
314
|
+
if (isRpcPortTaken && usingDifferentProcessRpc)
|
|
315
|
+
return;
|
|
316
|
+
if (isRpcPortTaken) {
|
|
317
|
+
log(`PKC RPC is already running (${pkcRpcUrl}) by another program. bitsocial-cli will use the running RPC server, and if shuts down, bitsocial-cli will start a new RPC instance`);
|
|
318
|
+
console.log("Using the already started RPC server at:", pkcRpcUrl);
|
|
319
|
+
console.log("bitsocial-cli daemon will monitor the PKC RPC and kubo ipfs API to make sure they're always up");
|
|
320
|
+
const PKC = await import("@pkcprotocol/pkc-js");
|
|
321
|
+
const pkc = await PKC.default({ pkcRpcClientsOptions: [pkcRpcUrl.toString()] });
|
|
322
|
+
await new Promise((resolve) => pkc.once("communitieschange", resolve));
|
|
323
|
+
pkc.on("error", (error) => console.error("Error from pkc instance", error));
|
|
324
|
+
console.log(`Communities in data path: `, pkc.communities);
|
|
325
|
+
usingDifferentProcessRpc = true;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Load installed challenge packages before starting the RPC server
|
|
329
|
+
const loadedChallenges = await loadChallengesIntoPKC(mergedPkcOptions.dataPath);
|
|
330
|
+
if (loadedChallenges.length > 0)
|
|
331
|
+
console.log(`Loaded challenge packages: ${loadedChallenges.join(", ")}`);
|
|
332
|
+
daemonServer = await startDaemonServer(pkcRpcUrl, ipfsGatewayEndpoint, mergedPkcOptions);
|
|
333
|
+
usingDifferentProcessRpc = false;
|
|
334
|
+
startedOwnRpc = true;
|
|
335
|
+
console.log(`pkc rpc: listening on ${pkcRpcUrl} (local connections only)`);
|
|
336
|
+
console.log(`pkc rpc: listening on ${pkcRpcUrl}${daemonServer.rpcAuthKey} (secret auth key for remote connections)`);
|
|
337
|
+
console.log(`Bitsocial data path: ${path.resolve(mergedPkcOptions.dataPath)}`);
|
|
338
|
+
console.log(`Communities in data path: `, daemonServer.listedSub);
|
|
339
|
+
const localIpAddress = "localhost";
|
|
340
|
+
const remoteIpAddress = getLanIpV4Address() || localIpAddress;
|
|
341
|
+
const rpcPort = pkcRpcUrl.port;
|
|
342
|
+
const webuiDescriptions = {
|
|
343
|
+
plebones: "A bare bones UI client",
|
|
344
|
+
seedit: "Similar to old reddit UI",
|
|
345
|
+
"5chan": "Imageboard-style UI"
|
|
346
|
+
};
|
|
347
|
+
for (const webui of daemonServer.webuis) {
|
|
348
|
+
const desc = webuiDescriptions[webui.name] ? ` - ${webuiDescriptions[webui.name]}` : "";
|
|
349
|
+
console.log(`WebUI (${webui.name}${desc}): http://${localIpAddress}:${rpcPort}${webui.endpointRemote}`);
|
|
350
|
+
if (remoteIpAddress !== localIpAddress)
|
|
351
|
+
console.log(`WebUI (${webui.name}${desc}): http://${remoteIpAddress}:${rpcPort}${webui.endpointRemote}`);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const isRpcPortTaken = await tcpPortUsed.check(Number(pkcRpcUrl.port), pkcRpcUrl.hostname);
|
|
355
|
+
if (!pkcOptionsFromFlag?.kuboRpcClientsOptions && !isRpcPortTaken && !usingDifferentProcessRpc)
|
|
356
|
+
await keepKuboUp();
|
|
357
|
+
await createOrConnectRpc();
|
|
358
|
+
let keepKuboUpInterval;
|
|
359
|
+
const { asyncExitHook } = await import("exit-hook");
|
|
360
|
+
const killKuboProcessGroup = (pid, signal) => {
|
|
361
|
+
// Kill the entire process group (negative PID) on non-Windows.
|
|
362
|
+
// Kubo is spawned with detached: true, so it has its own process group.
|
|
363
|
+
if (process.platform !== "win32") {
|
|
364
|
+
try {
|
|
365
|
+
process.kill(-pid, signal);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
/* best effort */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
process.kill(pid, signal);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
/* best effort */
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const killKuboProcess = async () => {
|
|
379
|
+
if (pendingKuboStart) {
|
|
380
|
+
try {
|
|
381
|
+
await pendingKuboStart;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
/* ignore */
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (kuboProcess?.pid && !kuboProcess.killed) {
|
|
388
|
+
const pid = kuboProcess.pid;
|
|
389
|
+
log("Attempting to kill kubo process with pid", pid);
|
|
390
|
+
try {
|
|
391
|
+
killKuboProcessGroup(pid, "SIGINT");
|
|
392
|
+
const exited = await new Promise((resolve) => {
|
|
393
|
+
const timeout = setTimeout(() => resolve(false), 5000);
|
|
394
|
+
kuboProcess?.once("exit", () => {
|
|
395
|
+
clearTimeout(timeout);
|
|
396
|
+
resolve(true);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
if (!exited) {
|
|
400
|
+
log("Kubo process did not exit after SIGINT, escalating to SIGKILL");
|
|
401
|
+
killKuboProcessGroup(pid, "SIGKILL");
|
|
402
|
+
}
|
|
403
|
+
log("Kubo process killed with pid", pid);
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
if (e instanceof Error && "code" in e && e.code === "ESRCH")
|
|
407
|
+
log("Kubo process already killed");
|
|
408
|
+
else
|
|
409
|
+
log.error("Error killing kubo process", e);
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
kuboProcess?.removeAllListeners();
|
|
413
|
+
kuboProcess = undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
asyncExitHook(async () => {
|
|
418
|
+
if (keepKuboUpInterval)
|
|
419
|
+
clearInterval(keepKuboUpInterval);
|
|
420
|
+
if (mainProcessExited)
|
|
421
|
+
return; // we already exited
|
|
422
|
+
console.log("\nShutting down Bitsocial daemon, it may take a few seconds to shut down all communities and the IPFS node...");
|
|
423
|
+
log("Received signal to exit, shutting down both kubo and pkc rpc. Please wait, it may take a few seconds");
|
|
424
|
+
mainProcessExited = true;
|
|
425
|
+
// Start killing Kubo immediately, in parallel with daemon server destroy.
|
|
426
|
+
// This way Kubo receives SIGINT right away, even if daemonServer.destroy() hangs.
|
|
427
|
+
const kuboKillPromise = killKuboProcess();
|
|
428
|
+
if (daemonServer)
|
|
429
|
+
try {
|
|
430
|
+
await daemonServer.destroy();
|
|
431
|
+
log("Daemon server shut down");
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
log.error("Error shutting down daemon server", e);
|
|
435
|
+
}
|
|
436
|
+
await kuboKillPromise;
|
|
437
|
+
}, { wait: 120000 } // could take two minutes to shut down
|
|
438
|
+
);
|
|
439
|
+
// Emergency cleanup: if the process force-exits (e.g. double Ctrl+C),
|
|
440
|
+
// synchronously SIGKILL kubo's process group. This is a no-op if
|
|
441
|
+
// killKuboProcess() already ran (it sets kuboProcess = undefined).
|
|
442
|
+
process.on("exit", () => {
|
|
443
|
+
if (kuboProcess?.pid) {
|
|
444
|
+
killKuboProcessGroup(kuboProcess.pid, "SIGKILL");
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
keepKuboUpInterval = setInterval(async () => {
|
|
448
|
+
if (mainProcessExited)
|
|
449
|
+
return;
|
|
450
|
+
const isRpcPortTaken = await tcpPortUsed.check(Number(pkcRpcUrl.port), pkcRpcUrl.hostname);
|
|
451
|
+
try {
|
|
452
|
+
if (!pkcOptionsFromFlag?.kuboRpcClientsOptions && !isRpcPortTaken && !usingDifferentProcessRpc)
|
|
453
|
+
await keepKuboUp();
|
|
454
|
+
else if (pkcOptionsFromFlag?.kuboRpcClientsOptions && !usingDifferentProcessRpc)
|
|
455
|
+
await keepKuboUp();
|
|
456
|
+
// Retry if kubo died and onKuboExit's restart attempt failed (e.g. transient port conflict)
|
|
457
|
+
else if (!kuboProcess && !pendingKuboStart && !usingDifferentProcessRpc)
|
|
458
|
+
await keepKuboUp();
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
log.trace(`keepKuboUp error (will retry): ${error instanceof Error ? error.message : String(error)}`);
|
|
462
|
+
}
|
|
463
|
+
await createOrConnectRpc();
|
|
464
|
+
}, 5000);
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
468
|
+
stdoutWrite(`\nDaemon failed to start: ${errorMsg}\n\n`);
|
|
469
|
+
// Show last 10 lines from log for context
|
|
470
|
+
try {
|
|
471
|
+
const logContent = fs.readFileSync(logFilePath, "utf-8");
|
|
472
|
+
const lines = logContent.trimEnd().split("\n");
|
|
473
|
+
const lastLines = lines.slice(-10).join("\n");
|
|
474
|
+
stdoutWrite(`Last log lines:\n${lastLines}\n\n`);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
/* log file might not exist yet */
|
|
478
|
+
}
|
|
479
|
+
stdoutWrite(`Full log: ${logFilePath}\n`);
|
|
480
|
+
stdoutWrite(`Or run: bitsocial logs\n`);
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
interface LogEntry {
|
|
3
|
+
timestamp: Date | null;
|
|
4
|
+
lines: string[];
|
|
5
|
+
}
|
|
6
|
+
export default class Logs extends Command {
|
|
7
|
+
static description: string;
|
|
8
|
+
static flags: {
|
|
9
|
+
follow: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
tail: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
since: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
until: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
logPath: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
};
|
|
15
|
+
static examples: string[];
|
|
16
|
+
private _findLatestLogFile;
|
|
17
|
+
_parseTimestamp(value: string): Date;
|
|
18
|
+
_extractTimestamp(line: string): Date | null;
|
|
19
|
+
_parseLogEntries(content: string): LogEntry[];
|
|
20
|
+
_filterEntries(entries: LogEntry[], since?: Date, until?: Date): LogEntry[];
|
|
21
|
+
_tailEntries(entries: LogEntry[], tailValue: string): LogEntry[];
|
|
22
|
+
run(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export {};
|