@holochain/hc-spin 0.600.0-dev.0 → 0.600.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/.github/workflows/test.yaml +8 -2
- package/CHANGELOG.md +11 -0
- package/README.md +15 -7
- package/dist/main/index.js +309 -295
- package/docs/DEVSETUP.md +12 -21
- package/electron.vite.config.ts +6 -0
- package/flake.lock +234 -0
- package/flake.nix +31 -0
- package/package.json +9 -8
- package/src/main/index.ts +30 -20
- package/src/main/menu.ts +23 -23
- package/src/main/validateArgs.ts +16 -1
- package/src/main/vite-env.d.ts +3 -0
- package/src/main/windows.ts +6 -5
- package/tsconfig.node.json +3 -2
package/dist/main/index.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const electron = require("electron");
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const node_crypto = require("node:crypto");
|
|
6
|
-
const commander = require("commander");
|
|
7
|
-
const contextMenu = require("electron-context-menu");
|
|
8
|
-
const split = require("split");
|
|
9
|
-
const childProcess = require("child_process");
|
|
10
|
-
const url = require("url");
|
|
11
|
-
const utils = require("@electron-toolkit/utils");
|
|
12
|
-
const net$1 = require("node:net");
|
|
13
|
-
const os = require("node:os");
|
|
14
2
|
const msgpack = require("@msgpack/msgpack");
|
|
15
3
|
const require$$0$3 = require("events");
|
|
16
4
|
const require$$1$1 = require("https");
|
|
@@ -19,10 +7,22 @@ const require$$3 = require("net");
|
|
|
19
7
|
const require$$4 = require("tls");
|
|
20
8
|
const require$$1 = require("crypto");
|
|
21
9
|
const require$$0$2 = require("stream");
|
|
10
|
+
const require$$7 = require("url");
|
|
22
11
|
const require$$0 = require("zlib");
|
|
23
12
|
const require$$0$1 = require("buffer");
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
24
15
|
const require$$2 = require("os");
|
|
25
16
|
const jsSha512 = require("js-sha512");
|
|
17
|
+
const childProcess = require("child_process");
|
|
18
|
+
const commander = require("commander");
|
|
19
|
+
const electron = require("electron");
|
|
20
|
+
const contextMenu = require("electron-context-menu");
|
|
21
|
+
const net$1 = require("node:net");
|
|
22
|
+
const os = require("node:os");
|
|
23
|
+
const node_crypto = require("node:crypto");
|
|
24
|
+
const split = require("split");
|
|
25
|
+
const utils = require("@electron-toolkit/utils");
|
|
26
26
|
function _interopNamespaceDefault(e) {
|
|
27
27
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
28
28
|
if (e) {
|
|
@@ -40,229 +40,6 @@ function _interopNamespaceDefault(e) {
|
|
|
40
40
|
return Object.freeze(n);
|
|
41
41
|
}
|
|
42
42
|
const childProcess__namespace = /* @__PURE__ */ _interopNamespaceDefault(childProcess);
|
|
43
|
-
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
44
|
-
const POOL_SIZE_MULTIPLIER = 128;
|
|
45
|
-
let pool, poolOffset;
|
|
46
|
-
function fillPool(bytes) {
|
|
47
|
-
if (!pool || pool.length < bytes) {
|
|
48
|
-
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
49
|
-
node_crypto.webcrypto.getRandomValues(pool);
|
|
50
|
-
poolOffset = 0;
|
|
51
|
-
} else if (poolOffset + bytes > pool.length) {
|
|
52
|
-
node_crypto.webcrypto.getRandomValues(pool);
|
|
53
|
-
poolOffset = 0;
|
|
54
|
-
}
|
|
55
|
-
poolOffset += bytes;
|
|
56
|
-
}
|
|
57
|
-
function nanoid(size = 21) {
|
|
58
|
-
fillPool(size -= 0);
|
|
59
|
-
let id = "";
|
|
60
|
-
for (let i = poolOffset - size; i < poolOffset; i++) {
|
|
61
|
-
id += urlAlphabet[pool[i] & 63];
|
|
62
|
-
}
|
|
63
|
-
return id;
|
|
64
|
-
}
|
|
65
|
-
async function createHappWindow(uiSource, appId, agentNum, appPort, appAuthToken, appDataRootDir) {
|
|
66
|
-
if (!appPort) throw new Error("App port not defined.");
|
|
67
|
-
const partition = `persist:${agentNum}:${appId}`;
|
|
68
|
-
if (uiSource.type === "path") {
|
|
69
|
-
const ses = electron.session.fromPartition(partition);
|
|
70
|
-
ses.protocol.handle("webhapp", async (request) => {
|
|
71
|
-
const uriWithoutProtocol = request.url.slice("webhapp://".length);
|
|
72
|
-
const filePathComponents = uriWithoutProtocol.split("/").slice(1);
|
|
73
|
-
const filePath = path.join(...filePathComponents);
|
|
74
|
-
return electron.net.fetch(url.pathToFileURL(path.join(uiSource.path, filePath)).toString());
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
let preloadScript = fs.readFileSync(path.join(__dirname, "../preload/index.js")).toString();
|
|
78
|
-
preloadScript += `
|
|
79
|
-
electron.contextBridge.exposeInMainWorld("__HC_LAUNCHER_ENV__", {
|
|
80
|
-
APP_INTERFACE_PORT: ${appPort},
|
|
81
|
-
INSTALLED_APP_ID: "${appId}",
|
|
82
|
-
APP_INTERFACE_TOKEN: [${appAuthToken}],
|
|
83
|
-
});
|
|
84
|
-
`;
|
|
85
|
-
const preloadPath = path.join(appDataRootDir, `preload-${agentNum}-${appId}.js`);
|
|
86
|
-
fs.writeFileSync(preloadPath, preloadScript);
|
|
87
|
-
let icon;
|
|
88
|
-
if (uiSource.type === "path") {
|
|
89
|
-
const iconPath = path.join(uiSource.path, "icon.png");
|
|
90
|
-
if (!fs.existsSync(iconPath) && agentNum === 1) {
|
|
91
|
-
console.warn(
|
|
92
|
-
"\n\n+++++ WARNING +++++\n[hc-spin] No icon.png found. It is recommended to put an icon.png file (1024x1024 pixel) in the root of your UI assets directory which can be used by the Holochain Launcher.\n+++++++++++++++++++\n\n"
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
icon = electron.nativeImage.createFromPath(iconPath);
|
|
96
|
-
} else {
|
|
97
|
-
try {
|
|
98
|
-
const iconResponse = await electron.net.fetch(`http://localhost:${uiSource.port}/icon.png`);
|
|
99
|
-
if (iconResponse.status === 404 && agentNum === 1) {
|
|
100
|
-
console.warn(
|
|
101
|
-
"\n\n+++++ WARNING +++++\n[hc-spin] No icon.png found. It is recommended to put an icon.png file (1024x1024 pixel) in the root of your UI assets directory which can be used by the Holochain Launcher.\n+++++++++++++++++++\n\n"
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
const buffer = await iconResponse.arrayBuffer();
|
|
105
|
-
icon = electron.nativeImage.createFromBuffer(Buffer.from(buffer));
|
|
106
|
-
} catch (e) {
|
|
107
|
-
console.error("Failed to get icon.png: ", e);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return new electron.BrowserWindow({
|
|
111
|
-
width: 1200,
|
|
112
|
-
height: 800,
|
|
113
|
-
show: false,
|
|
114
|
-
icon,
|
|
115
|
-
title: `Agent ${agentNum} - ${appId}`,
|
|
116
|
-
webPreferences: {
|
|
117
|
-
preload: preloadPath,
|
|
118
|
-
partition
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
async function loadHappWindow(happWindow, uiSource, happOrWebhappPath, agentNum, openDevtools) {
|
|
123
|
-
const [windowPositionX, windowPositionY] = happWindow.getPosition();
|
|
124
|
-
const windowPositionXMoved = windowPositionX + agentNum * 20;
|
|
125
|
-
const windowPositionYMoved = windowPositionY + agentNum * 20;
|
|
126
|
-
happWindow.setPosition(windowPositionXMoved, windowPositionYMoved);
|
|
127
|
-
happWindow.menuBarVisible = false;
|
|
128
|
-
setLinkOpenHandlers(happWindow);
|
|
129
|
-
happWindow.on("page-title-updated", (evt) => {
|
|
130
|
-
evt.preventDefault();
|
|
131
|
-
});
|
|
132
|
-
if (openDevtools) happWindow.webContents.openDevTools();
|
|
133
|
-
if (uiSource.type === "port") {
|
|
134
|
-
try {
|
|
135
|
-
await electron.net.fetch(`http://localhost:${uiSource.port}/index.html`);
|
|
136
|
-
} catch (e) {
|
|
137
|
-
console.error(`No index.html file found at http://localhost:${uiSource.port}/index.html`, e);
|
|
138
|
-
if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
|
139
|
-
happWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
|
140
|
-
} else {
|
|
141
|
-
happWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
|
142
|
-
}
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
await happWindow.loadURL(`http://localhost:${uiSource.port}`);
|
|
146
|
-
} else if (uiSource.type === "path") {
|
|
147
|
-
try {
|
|
148
|
-
await happWindow.loadURL(`webhapp://webhappwindow/index.html`);
|
|
149
|
-
} catch (e) {
|
|
150
|
-
console.error("[ERROR] Failed to fetch index.html");
|
|
151
|
-
if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
|
152
|
-
happWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
|
153
|
-
} else {
|
|
154
|
-
const notFoundPath = happOrWebhappPath.type === "webhapp" ? path.join(__dirname, "../renderer/indexNotFound1.html") : path.join(__dirname, "../renderer/indexNotFound2.html");
|
|
155
|
-
happWindow.loadFile(notFoundPath);
|
|
156
|
-
}
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
throw new Error("Unsupported uiSource type: ", uiSource.type);
|
|
161
|
-
}
|
|
162
|
-
happWindow.show();
|
|
163
|
-
}
|
|
164
|
-
function setLinkOpenHandlers(browserWindow) {
|
|
165
|
-
browserWindow.webContents.on("will-navigate", (e) => {
|
|
166
|
-
if (e.url.startsWith("http://localhost") || e.url.startsWith("http://127.0.0.1")) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (e.url.startsWith("http://") || e.url.startsWith("https://") || e.url.startsWith("mailto://")) {
|
|
170
|
-
e.preventDefault();
|
|
171
|
-
electron.shell.openExternal(e.url);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
browserWindow.webContents.setWindowOpenHandler((details) => {
|
|
175
|
-
if (details.url.startsWith("http://") || details.url.startsWith("https://")) {
|
|
176
|
-
electron.shell.openExternal(details.url);
|
|
177
|
-
}
|
|
178
|
-
return { action: "deny" };
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
class Locked extends Error {
|
|
182
|
-
constructor(port) {
|
|
183
|
-
super(`${port} is locked`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const lockedPorts = {
|
|
187
|
-
old: /* @__PURE__ */ new Set(),
|
|
188
|
-
young: /* @__PURE__ */ new Set()
|
|
189
|
-
};
|
|
190
|
-
const releaseOldLockedPortsIntervalMs = 1e3 * 15;
|
|
191
|
-
let timeout;
|
|
192
|
-
const getLocalHosts = () => {
|
|
193
|
-
const interfaces = os.networkInterfaces();
|
|
194
|
-
const results = /* @__PURE__ */ new Set([void 0, "0.0.0.0"]);
|
|
195
|
-
for (const _interface of Object.values(interfaces)) {
|
|
196
|
-
for (const config of _interface) {
|
|
197
|
-
results.add(config.address);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return results;
|
|
201
|
-
};
|
|
202
|
-
const checkAvailablePort = (options) => new Promise((resolve, reject) => {
|
|
203
|
-
const server = net$1.createServer();
|
|
204
|
-
server.unref();
|
|
205
|
-
server.on("error", reject);
|
|
206
|
-
server.listen(options, () => {
|
|
207
|
-
const { port } = server.address();
|
|
208
|
-
server.close(() => {
|
|
209
|
-
resolve(port);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
const getAvailablePort = async (options, hosts) => {
|
|
214
|
-
if (options.host || options.port === 0) {
|
|
215
|
-
return checkAvailablePort(options);
|
|
216
|
-
}
|
|
217
|
-
for (const host of hosts) {
|
|
218
|
-
try {
|
|
219
|
-
await checkAvailablePort({ port: options.port, host });
|
|
220
|
-
} catch (error) {
|
|
221
|
-
if (!["EADDRNOTAVAIL", "EINVAL"].includes(error.code)) {
|
|
222
|
-
throw error;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return options.port;
|
|
227
|
-
};
|
|
228
|
-
const portCheckSequence = function* (ports) {
|
|
229
|
-
yield 0;
|
|
230
|
-
};
|
|
231
|
-
async function getPorts(options) {
|
|
232
|
-
let exclude = /* @__PURE__ */ new Set();
|
|
233
|
-
if (timeout === void 0) {
|
|
234
|
-
timeout = setTimeout(() => {
|
|
235
|
-
timeout = void 0;
|
|
236
|
-
lockedPorts.old = lockedPorts.young;
|
|
237
|
-
lockedPorts.young = /* @__PURE__ */ new Set();
|
|
238
|
-
}, releaseOldLockedPortsIntervalMs);
|
|
239
|
-
if (timeout.unref) {
|
|
240
|
-
timeout.unref();
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
const hosts = getLocalHosts();
|
|
244
|
-
for (const port of portCheckSequence()) {
|
|
245
|
-
try {
|
|
246
|
-
if (exclude.has(port)) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
let availablePort = await getAvailablePort({ ...options, port }, hosts);
|
|
250
|
-
while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) {
|
|
251
|
-
if (port !== 0) {
|
|
252
|
-
throw new Locked(port);
|
|
253
|
-
}
|
|
254
|
-
availablePort = await getAvailablePort({ ...options, port }, hosts);
|
|
255
|
-
}
|
|
256
|
-
lockedPorts.young.add(availablePort);
|
|
257
|
-
return availablePort;
|
|
258
|
-
} catch (error) {
|
|
259
|
-
if (!["EADDRINUSE", "EACCES"].includes(error.code) && !(error instanceof Locked)) {
|
|
260
|
-
throw error;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
throw new Error("No available ports found");
|
|
265
|
-
}
|
|
266
43
|
var CellType;
|
|
267
44
|
(function(CellType2) {
|
|
268
45
|
CellType2["Provisioned"] = "provisioned";
|
|
@@ -3096,7 +2873,7 @@ const net = require$$3;
|
|
|
3096
2873
|
const tls = require$$4;
|
|
3097
2874
|
const { randomBytes, createHash: createHash$1 } = require$$1;
|
|
3098
2875
|
const { Duplex: Duplex$2, Readable } = require$$0$2;
|
|
3099
|
-
const { URL: URL$1 } =
|
|
2876
|
+
const { URL: URL$1 } = require$$7;
|
|
3100
2877
|
const PerMessageDeflate$1 = permessageDeflate;
|
|
3101
2878
|
const Receiver2 = receiver;
|
|
3102
2879
|
const Sender2 = sender;
|
|
@@ -11982,12 +11759,12 @@ class WsClient extends Emittery {
|
|
|
11982
11759
|
pendingRequests;
|
|
11983
11760
|
index;
|
|
11984
11761
|
authenticationToken;
|
|
11985
|
-
constructor(socket,
|
|
11762
|
+
constructor(socket, url, options) {
|
|
11986
11763
|
super();
|
|
11987
11764
|
this.registerMessageListener(socket);
|
|
11988
11765
|
this.registerCloseListener(socket);
|
|
11989
11766
|
this.socket = socket;
|
|
11990
|
-
this.url =
|
|
11767
|
+
this.url = url;
|
|
11991
11768
|
this.options = options;
|
|
11992
11769
|
this.pendingRequests = {};
|
|
11993
11770
|
this.index = 0;
|
|
@@ -11999,14 +11776,14 @@ class WsClient extends Emittery {
|
|
|
11999
11776
|
* @param options - Options for the WsClient.
|
|
12000
11777
|
* @returns An new instance of the WsClient.
|
|
12001
11778
|
*/
|
|
12002
|
-
static connect(
|
|
11779
|
+
static connect(url, options) {
|
|
12003
11780
|
return new Promise((resolve, reject) => {
|
|
12004
|
-
const socket = new IsoWebSocket(
|
|
11781
|
+
const socket = new IsoWebSocket(url, options);
|
|
12005
11782
|
socket.addEventListener("error", (errorEvent) => {
|
|
12006
|
-
reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${
|
|
11783
|
+
reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.error}`));
|
|
12007
11784
|
});
|
|
12008
11785
|
socket.addEventListener("open", (_) => {
|
|
12009
|
-
const client = new WsClient(socket,
|
|
11786
|
+
const client = new WsClient(socket, url, options);
|
|
12010
11787
|
resolve(client);
|
|
12011
11788
|
}, { once: true });
|
|
12012
11789
|
});
|
|
@@ -12152,12 +11929,12 @@ class WsClient extends Emittery {
|
|
|
12152
11929
|
}
|
|
12153
11930
|
}, { once: true });
|
|
12154
11931
|
}
|
|
12155
|
-
async reconnectWebsocket(
|
|
11932
|
+
async reconnectWebsocket(url, token) {
|
|
12156
11933
|
return new Promise((resolve, reject) => {
|
|
12157
|
-
this.socket = new IsoWebSocket(
|
|
11934
|
+
this.socket = new IsoWebSocket(url, this.options);
|
|
12158
11935
|
this.socket.addEventListener("error", (errorEvent) => {
|
|
12159
11936
|
this.authenticationToken = void 0;
|
|
12160
|
-
reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${
|
|
11937
|
+
reject(new HolochainError("ConnectionError", `could not connect to Holochain Conductor API at ${url} - ${errorEvent.message}`));
|
|
12161
11938
|
}, { once: true });
|
|
12162
11939
|
const invalidTokenCloseListener = (closeEvent) => {
|
|
12163
11940
|
this.authenticationToken = void 0;
|
|
@@ -12420,53 +12197,112 @@ var ChainOpType;
|
|
|
12420
12197
|
ChainOpType2["RegisterAddLink"] = "RegisterAddLink";
|
|
12421
12198
|
ChainOpType2["RegisterRemoveLink"] = "RegisterRemoveLink";
|
|
12422
12199
|
})(ChainOpType || (ChainOpType = {}));
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
`hc spin takes exactly one argument (the path to the .happ or .webhapp file) but got ${cliArgs.length} arguments: ${cliArgs}`
|
|
12427
|
-
);
|
|
12200
|
+
class Locked extends Error {
|
|
12201
|
+
constructor(port) {
|
|
12202
|
+
super(`${port} is locked`);
|
|
12428
12203
|
}
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12433
|
-
|
|
12204
|
+
}
|
|
12205
|
+
const lockedPorts = {
|
|
12206
|
+
old: /* @__PURE__ */ new Set(),
|
|
12207
|
+
young: /* @__PURE__ */ new Set()
|
|
12208
|
+
};
|
|
12209
|
+
const releaseOldLockedPortsIntervalMs = 1e3 * 15;
|
|
12210
|
+
let timeout;
|
|
12211
|
+
const getLocalHosts = () => {
|
|
12212
|
+
const interfaces = os.networkInterfaces();
|
|
12213
|
+
const results = /* @__PURE__ */ new Set([void 0, "0.0.0.0"]);
|
|
12214
|
+
for (const _interface of Object.values(interfaces)) {
|
|
12215
|
+
for (const config of _interface) {
|
|
12216
|
+
results.add(config.address);
|
|
12217
|
+
}
|
|
12434
12218
|
}
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12219
|
+
return results;
|
|
12220
|
+
};
|
|
12221
|
+
const checkAvailablePort = (options) => new Promise((resolve, reject) => {
|
|
12222
|
+
const server = net$1.createServer();
|
|
12223
|
+
server.unref();
|
|
12224
|
+
server.on("error", reject);
|
|
12225
|
+
server.listen(options, () => {
|
|
12226
|
+
const { port } = server.address();
|
|
12227
|
+
server.close(() => {
|
|
12228
|
+
resolve(port);
|
|
12229
|
+
});
|
|
12230
|
+
});
|
|
12231
|
+
});
|
|
12232
|
+
const getAvailablePort = async (options, hosts) => {
|
|
12233
|
+
if (options.host || options.port === 0) {
|
|
12234
|
+
return checkAvailablePort(options);
|
|
12439
12235
|
}
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
)
|
|
12236
|
+
for (const host of hosts) {
|
|
12237
|
+
try {
|
|
12238
|
+
await checkAvailablePort({ port: options.port, host });
|
|
12239
|
+
} catch (error) {
|
|
12240
|
+
if (!["EADDRNOTAVAIL", "EINVAL"].includes(error.code)) {
|
|
12241
|
+
throw error;
|
|
12242
|
+
}
|
|
12243
|
+
}
|
|
12444
12244
|
}
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12245
|
+
return options.port;
|
|
12246
|
+
};
|
|
12247
|
+
const portCheckSequence = function* (ports) {
|
|
12248
|
+
yield 0;
|
|
12249
|
+
};
|
|
12250
|
+
async function getPorts(options) {
|
|
12251
|
+
let exclude = /* @__PURE__ */ new Set();
|
|
12252
|
+
if (timeout === void 0) {
|
|
12253
|
+
timeout = setTimeout(() => {
|
|
12254
|
+
timeout = void 0;
|
|
12255
|
+
lockedPorts.old = lockedPorts.young;
|
|
12256
|
+
lockedPorts.young = /* @__PURE__ */ new Set();
|
|
12257
|
+
}, releaseOldLockedPortsIntervalMs);
|
|
12258
|
+
if (timeout.unref) {
|
|
12259
|
+
timeout.unref();
|
|
12260
|
+
}
|
|
12450
12261
|
}
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12262
|
+
const hosts = getLocalHosts();
|
|
12263
|
+
for (const port of portCheckSequence()) {
|
|
12264
|
+
try {
|
|
12265
|
+
if (exclude.has(port)) {
|
|
12266
|
+
continue;
|
|
12267
|
+
}
|
|
12268
|
+
let availablePort = await getAvailablePort({ ...options, port }, hosts);
|
|
12269
|
+
while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) {
|
|
12270
|
+
if (port !== 0) {
|
|
12271
|
+
throw new Locked(port);
|
|
12272
|
+
}
|
|
12273
|
+
availablePort = await getAvailablePort({ ...options, port }, hosts);
|
|
12274
|
+
}
|
|
12275
|
+
lockedPorts.young.add(availablePort);
|
|
12276
|
+
return availablePort;
|
|
12277
|
+
} catch (error) {
|
|
12278
|
+
if (!["EADDRINUSE", "EACCES"].includes(error.code) && !(error instanceof Locked)) {
|
|
12279
|
+
throw error;
|
|
12280
|
+
}
|
|
12281
|
+
}
|
|
12455
12282
|
}
|
|
12456
|
-
|
|
12457
|
-
|
|
12458
|
-
|
|
12459
|
-
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
}
|
|
12283
|
+
throw new Error("No available ports found");
|
|
12284
|
+
}
|
|
12285
|
+
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
12286
|
+
const POOL_SIZE_MULTIPLIER = 128;
|
|
12287
|
+
let pool, poolOffset;
|
|
12288
|
+
function fillPool(bytes) {
|
|
12289
|
+
if (!pool || pool.length < bytes) {
|
|
12290
|
+
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
12291
|
+
node_crypto.webcrypto.getRandomValues(pool);
|
|
12292
|
+
poolOffset = 0;
|
|
12293
|
+
} else if (poolOffset + bytes > pool.length) {
|
|
12294
|
+
node_crypto.webcrypto.getRandomValues(pool);
|
|
12295
|
+
poolOffset = 0;
|
|
12296
|
+
}
|
|
12297
|
+
poolOffset += bytes;
|
|
12298
|
+
}
|
|
12299
|
+
function nanoid(size = 21) {
|
|
12300
|
+
fillPool(size -= 0);
|
|
12301
|
+
let id = "";
|
|
12302
|
+
for (let i = poolOffset - size; i < poolOffset; i++) {
|
|
12303
|
+
id += urlAlphabet[pool[i] & 63];
|
|
12304
|
+
}
|
|
12305
|
+
return id;
|
|
12470
12306
|
}
|
|
12471
12307
|
const menu = electron.Menu.buildFromTemplate([
|
|
12472
12308
|
{
|
|
@@ -12555,11 +12391,179 @@ const menu = electron.Menu.buildFromTemplate([
|
|
|
12555
12391
|
]
|
|
12556
12392
|
}
|
|
12557
12393
|
]);
|
|
12394
|
+
function validateCliArgs(cliArgs, cliOpts, appDataRootDir) {
|
|
12395
|
+
if (cliArgs.length !== 1) {
|
|
12396
|
+
throw new Error(
|
|
12397
|
+
`hc spin takes exactly one argument (the path to the .happ or .webhapp file) but got ${cliArgs.length} arguments: ${cliArgs}`
|
|
12398
|
+
);
|
|
12399
|
+
}
|
|
12400
|
+
const happOrWebhappPath = cliArgs[0];
|
|
12401
|
+
if (!happOrWebhappPath.endsWith(".happ") && !happOrWebhappPath.endsWith(".webhapp")) {
|
|
12402
|
+
throw new Error(
|
|
12403
|
+
`The path passed to hc spin must either be a .happ or a .webhapp file but got path '${happOrWebhappPath}'`
|
|
12404
|
+
);
|
|
12405
|
+
}
|
|
12406
|
+
if (!fs.existsSync(happOrWebhappPath)) {
|
|
12407
|
+
throw new Error(
|
|
12408
|
+
`Path to .happ or .webhapp file passed as argument does not exist: ${happOrWebhappPath}`
|
|
12409
|
+
);
|
|
12410
|
+
}
|
|
12411
|
+
if (cliOpts.numAgents !== void 0 && (typeof cliOpts.numAgents !== "number" || Number.isNaN(cliOpts.numAgents))) {
|
|
12412
|
+
throw new Error(
|
|
12413
|
+
`The --num-agents (-n) option must be of type number but got: ${cliOpts.numAgents}`
|
|
12414
|
+
);
|
|
12415
|
+
}
|
|
12416
|
+
if (cliOpts.targetArcFactor !== void 0 && (typeof cliOpts.targetArcFactor !== "number" || Number.isNaN(cliOpts.targetArcFactor))) {
|
|
12417
|
+
throw new Error(
|
|
12418
|
+
`The --target-arc-factor (-t) option must be a valid number but got: ${cliOpts.targetArcFactor}`
|
|
12419
|
+
);
|
|
12420
|
+
}
|
|
12421
|
+
const isHapp = happOrWebhappPath.endsWith(".happ");
|
|
12422
|
+
if (isHapp && !cliOpts.uiPath && !cliOpts.uiPort) {
|
|
12423
|
+
throw new Error(
|
|
12424
|
+
"If you pass a .happ file as argument, you must also provide either the --ui-port or the --ui-path option pointing to the UI assets."
|
|
12425
|
+
);
|
|
12426
|
+
}
|
|
12427
|
+
if (cliOpts.uiPath && cliOpts.uiPort) {
|
|
12428
|
+
throw new Error(
|
|
12429
|
+
"Only one of --ui-port and --ui-path is allowed at the same time but got values for both."
|
|
12430
|
+
);
|
|
12431
|
+
}
|
|
12432
|
+
const appId = cliOpts.appId ? cliOpts.appId : path.parse(path.basename(cliArgs[0])).name;
|
|
12433
|
+
const holochainPath = cliOpts.holochainPath;
|
|
12434
|
+
const numAgents = cliOpts.numAgents ? cliOpts.numAgents : 2;
|
|
12435
|
+
return {
|
|
12436
|
+
appId,
|
|
12437
|
+
holochainPath,
|
|
12438
|
+
numAgents,
|
|
12439
|
+
networkSeed: cliOpts.networkSeed,
|
|
12440
|
+
targetArcFactor: cliOpts.targetArcFactor,
|
|
12441
|
+
uiSource: cliOpts.uiPath ? { type: "path", path: cliOpts.uiPath } : cliOpts.uiPort ? { type: "port", port: cliOpts.uiPort } : { type: "path", path: path.join(appDataRootDir, "apps", appId, "ui") },
|
|
12442
|
+
singalingUrl: cliOpts.signalingUrl,
|
|
12443
|
+
bootstrapUrl: cliOpts.bootstrapUrl,
|
|
12444
|
+
happOrWebhappPath: isHapp ? { type: "happ", path: happOrWebhappPath } : { type: "webhapp", path: happOrWebhappPath },
|
|
12445
|
+
openDevtools: cliOpts.openDevtools ? true : false
|
|
12446
|
+
};
|
|
12447
|
+
}
|
|
12448
|
+
async function createHappWindow(uiSource, appId, agentNum, appPort, appAuthToken, appDataRootDir) {
|
|
12449
|
+
if (!appPort) throw new Error("App port not defined.");
|
|
12450
|
+
const partition = `persist:${agentNum}:${appId}`;
|
|
12451
|
+
if (uiSource.type === "path") {
|
|
12452
|
+
const ses = electron.session.fromPartition(partition);
|
|
12453
|
+
ses.protocol.handle("webhapp", async (request) => {
|
|
12454
|
+
const uriWithoutProtocol = request.url.slice("webhapp://".length);
|
|
12455
|
+
const filePathComponents = uriWithoutProtocol.split("/").slice(1);
|
|
12456
|
+
const filePath = path.join(...filePathComponents);
|
|
12457
|
+
return electron.net.fetch(require$$7.pathToFileURL(path.join(uiSource.path, filePath)).toString());
|
|
12458
|
+
});
|
|
12459
|
+
}
|
|
12460
|
+
let preloadScript = fs.readFileSync(path.join(__dirname, "../preload/index.js")).toString();
|
|
12461
|
+
preloadScript += `
|
|
12462
|
+
electron.contextBridge.exposeInMainWorld("__HC_LAUNCHER_ENV__", {
|
|
12463
|
+
APP_INTERFACE_PORT: ${appPort},
|
|
12464
|
+
INSTALLED_APP_ID: "${appId}",
|
|
12465
|
+
APP_INTERFACE_TOKEN: [${appAuthToken}],
|
|
12466
|
+
});
|
|
12467
|
+
`;
|
|
12468
|
+
const preloadPath = path.join(appDataRootDir, `preload-${agentNum}-${appId}.js`);
|
|
12469
|
+
fs.writeFileSync(preloadPath, preloadScript);
|
|
12470
|
+
let icon;
|
|
12471
|
+
if (uiSource.type === "path") {
|
|
12472
|
+
const iconPath = path.join(uiSource.path, "icon.png");
|
|
12473
|
+
if (!fs.existsSync(iconPath) && agentNum === 1) {
|
|
12474
|
+
console.warn(
|
|
12475
|
+
"\n\n+++++ WARNING +++++\n[hc-spin] No icon.png found. It is recommended to put an icon.png file (1024x1024 pixel) in the root of your UI assets directory which can be used by the Holochain Launcher.\n+++++++++++++++++++\n\n"
|
|
12476
|
+
);
|
|
12477
|
+
}
|
|
12478
|
+
icon = electron.nativeImage.createFromPath(iconPath);
|
|
12479
|
+
} else {
|
|
12480
|
+
try {
|
|
12481
|
+
const iconResponse = await electron.net.fetch(`http://localhost:${uiSource.port}/icon.png`);
|
|
12482
|
+
if (iconResponse.status === 404 && agentNum === 1) {
|
|
12483
|
+
console.warn(
|
|
12484
|
+
"\n\n+++++ WARNING +++++\n[hc-spin] No icon.png found. It is recommended to put an icon.png file (1024x1024 pixel) in the root of your UI assets directory which can be used by the Holochain Launcher.\n+++++++++++++++++++\n\n"
|
|
12485
|
+
);
|
|
12486
|
+
}
|
|
12487
|
+
const buffer = await iconResponse.arrayBuffer();
|
|
12488
|
+
icon = electron.nativeImage.createFromBuffer(Buffer.from(buffer));
|
|
12489
|
+
} catch (e) {
|
|
12490
|
+
console.error("Failed to get icon.png: ", e);
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
return new electron.BrowserWindow({
|
|
12494
|
+
width: 1200,
|
|
12495
|
+
height: 800,
|
|
12496
|
+
show: false,
|
|
12497
|
+
icon,
|
|
12498
|
+
title: `Agent ${agentNum} - ${appId}`,
|
|
12499
|
+
webPreferences: {
|
|
12500
|
+
preload: preloadPath,
|
|
12501
|
+
partition
|
|
12502
|
+
}
|
|
12503
|
+
});
|
|
12504
|
+
}
|
|
12505
|
+
async function loadHappWindow(happWindow, uiSource, happOrWebhappPath, agentNum, openDevtools) {
|
|
12506
|
+
const [windowPositionX, windowPositionY] = happWindow.getPosition();
|
|
12507
|
+
const windowPositionXMoved = windowPositionX + agentNum * 20;
|
|
12508
|
+
const windowPositionYMoved = windowPositionY + agentNum * 20;
|
|
12509
|
+
happWindow.setPosition(windowPositionXMoved, windowPositionYMoved);
|
|
12510
|
+
happWindow.menuBarVisible = false;
|
|
12511
|
+
setLinkOpenHandlers(happWindow);
|
|
12512
|
+
happWindow.on("page-title-updated", (evt) => {
|
|
12513
|
+
evt.preventDefault();
|
|
12514
|
+
});
|
|
12515
|
+
if (openDevtools) happWindow.webContents.openDevTools();
|
|
12516
|
+
if (uiSource.type === "port") {
|
|
12517
|
+
try {
|
|
12518
|
+
await electron.net.fetch(`http://localhost:${uiSource.port}/index.html`);
|
|
12519
|
+
} catch (e) {
|
|
12520
|
+
console.error(`No index.html file found at http://localhost:${uiSource.port}/index.html`, e);
|
|
12521
|
+
if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
|
12522
|
+
happWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
|
12523
|
+
} else {
|
|
12524
|
+
happWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
|
12525
|
+
}
|
|
12526
|
+
return;
|
|
12527
|
+
}
|
|
12528
|
+
await happWindow.loadURL(`http://localhost:${uiSource.port}`);
|
|
12529
|
+
} else if (uiSource.type === "path") {
|
|
12530
|
+
try {
|
|
12531
|
+
await happWindow.loadURL(`webhapp://webhappwindow/index.html`);
|
|
12532
|
+
} catch (e) {
|
|
12533
|
+
console.error("[ERROR] Failed to fetch index.html");
|
|
12534
|
+
if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
|
12535
|
+
happWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
|
12536
|
+
} else {
|
|
12537
|
+
const notFoundPath = happOrWebhappPath.type === "webhapp" ? path.join(__dirname, "../renderer/indexNotFound1.html") : path.join(__dirname, "../renderer/indexNotFound2.html");
|
|
12538
|
+
happWindow.loadFile(notFoundPath);
|
|
12539
|
+
}
|
|
12540
|
+
return;
|
|
12541
|
+
}
|
|
12542
|
+
} else {
|
|
12543
|
+
throw new Error(`Unsupported uiSource: ${JSON.stringify(uiSource)}`);
|
|
12544
|
+
}
|
|
12545
|
+
happWindow.show();
|
|
12546
|
+
}
|
|
12547
|
+
function setLinkOpenHandlers(browserWindow) {
|
|
12548
|
+
browserWindow.webContents.on("will-navigate", (e) => {
|
|
12549
|
+
if (e.url.startsWith("http://localhost") || e.url.startsWith("http://127.0.0.1")) {
|
|
12550
|
+
return;
|
|
12551
|
+
}
|
|
12552
|
+
if (e.url.startsWith("http://") || e.url.startsWith("https://") || e.url.startsWith("mailto://")) {
|
|
12553
|
+
e.preventDefault();
|
|
12554
|
+
electron.shell.openExternal(e.url);
|
|
12555
|
+
}
|
|
12556
|
+
});
|
|
12557
|
+
browserWindow.webContents.setWindowOpenHandler((details) => {
|
|
12558
|
+
if (details.url.startsWith("http://") || details.url.startsWith("https://")) {
|
|
12559
|
+
electron.shell.openExternal(details.url);
|
|
12560
|
+
}
|
|
12561
|
+
return { action: "deny" };
|
|
12562
|
+
});
|
|
12563
|
+
}
|
|
12558
12564
|
const rustUtils = require("@holochain/hc-spin-rust-utils");
|
|
12559
|
-
const cliPackageJsonPath = path.resolve(path.join(electron.app.getAppPath(), "../../package.json"));
|
|
12560
|
-
const cliPackageJson = require(cliPackageJsonPath);
|
|
12561
12565
|
const cli = new commander.Command();
|
|
12562
|
-
cli.name("hc-spin").description("CLI to run Holochain apps during development.").version(`${
|
|
12566
|
+
cli.name("hc-spin").description("CLI to run Holochain apps during development.").version(`${"0.600.0"} (built for holochain ${"0.6.0"})`).argument(
|
|
12563
12567
|
"<path>",
|
|
12564
12568
|
"Path to .webhapp or .happ file to launch. If a .happ file is passed, either a UI path must be specified via --ui-path or a port pointing to a localhost server via --ui-port"
|
|
12565
12569
|
).option(
|
|
@@ -12572,7 +12576,12 @@ cli.name("hc-spin").description("CLI to run Holochain apps during development.")
|
|
|
12572
12576
|
new commander.Option("-n, --num-agents <number>", "How many agents to spawn the app for.").argParser(
|
|
12573
12577
|
parseInt
|
|
12574
12578
|
)
|
|
12575
|
-
).option("--network-seed <string>", "Install the app with a specific network seed.").
|
|
12579
|
+
).option("--network-seed <string>", "Install the app with a specific network seed.").addOption(
|
|
12580
|
+
new commander.Option(
|
|
12581
|
+
"-t, --target-arc-factor <number>",
|
|
12582
|
+
"Set the target arc factor for all conductors. In normal operation, leave this as the default 1. For leacher/zero-arc nodes that do not contribute to gossip, set to 0."
|
|
12583
|
+
).argParser(parseInt)
|
|
12584
|
+
).option("--ui-path <path>", "Path to the folder containing the index.html of the webhapp's UI.").option(
|
|
12576
12585
|
"--ui-port <number>",
|
|
12577
12586
|
"Port pointing to a localhost dev server that serves your UI assets."
|
|
12578
12587
|
).option(
|
|
@@ -12677,7 +12686,7 @@ async function startLocalServices() {
|
|
|
12677
12686
|
});
|
|
12678
12687
|
});
|
|
12679
12688
|
}
|
|
12680
|
-
async function spawnSandboxes(nAgents, happPath, bootStrapUrl, signalUrl, appId, networkSeed) {
|
|
12689
|
+
async function spawnSandboxes(nAgents, happPath, bootStrapUrl, signalUrl, appId, networkSeed, targetArcFactor) {
|
|
12681
12690
|
const generateArgs = [
|
|
12682
12691
|
"sandbox",
|
|
12683
12692
|
"--piped",
|
|
@@ -12689,7 +12698,7 @@ async function spawnSandboxes(nAgents, happPath, bootStrapUrl, signalUrl, appId,
|
|
|
12689
12698
|
"--run"
|
|
12690
12699
|
];
|
|
12691
12700
|
let appPorts = "";
|
|
12692
|
-
for (
|
|
12701
|
+
for (let i = 1; i <= nAgents; i++) {
|
|
12693
12702
|
const appPort = await getPorts();
|
|
12694
12703
|
appPorts += `${appPort},`;
|
|
12695
12704
|
}
|
|
@@ -12698,7 +12707,11 @@ async function spawnSandboxes(nAgents, happPath, bootStrapUrl, signalUrl, appId,
|
|
|
12698
12707
|
generateArgs.push("--network-seed");
|
|
12699
12708
|
generateArgs.push(networkSeed);
|
|
12700
12709
|
}
|
|
12701
|
-
generateArgs.push(happPath, "network"
|
|
12710
|
+
generateArgs.push(happPath, "network");
|
|
12711
|
+
if (targetArcFactor !== void 0) {
|
|
12712
|
+
generateArgs.push("--target-arc-factor", targetArcFactor.toString());
|
|
12713
|
+
}
|
|
12714
|
+
generateArgs.push("--bootstrap", bootStrapUrl, "webrtc", signalUrl);
|
|
12702
12715
|
let readyConductors = 0;
|
|
12703
12716
|
const portsInfo = {};
|
|
12704
12717
|
const sandboxPaths = [];
|
|
@@ -12749,7 +12762,8 @@ electron.app.whenReady().then(async () => {
|
|
|
12749
12762
|
CLI_OPTS.bootstrapUrl ? CLI_OPTS.bootstrapUrl : bootstrapUrl,
|
|
12750
12763
|
CLI_OPTS.singalingUrl ? CLI_OPTS.singalingUrl : signalingUrl,
|
|
12751
12764
|
CLI_OPTS.appId,
|
|
12752
|
-
CLI_OPTS.networkSeed
|
|
12765
|
+
CLI_OPTS.networkSeed,
|
|
12766
|
+
CLI_OPTS.targetArcFactor
|
|
12753
12767
|
);
|
|
12754
12768
|
const lairUrls = [];
|
|
12755
12769
|
sandboxPaths.forEach((sandbox) => {
|
|
@@ -12765,7 +12779,7 @@ electron.app.whenReady().then(async () => {
|
|
|
12765
12779
|
}
|
|
12766
12780
|
});
|
|
12767
12781
|
SANDBOX_PROCESSES.push(sandboxHandle);
|
|
12768
|
-
for (
|
|
12782
|
+
for (let i = 0; i < CLI_OPTS.numAgents; i++) {
|
|
12769
12783
|
const zomeCallSigner = await rustUtils.ZomeCallSigner.connect(lairUrls[i], "pass");
|
|
12770
12784
|
const adminPort = portsInfo[i].admin_port;
|
|
12771
12785
|
const adminWs = await AdminWebsocket.connect({
|