@holochain/hc-spin 0.100.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/.editorconfig +9 -0
- package/.eslintignore +4 -0
- package/.eslintrc +31 -0
- package/.prettierignore +6 -0
- package/.prettierrc.yaml +3 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +39 -0
- package/.vscode/settings.json +12 -0
- package/.yarnrc.yml +1 -0
- package/README.md +61 -0
- package/build/entitlements.mac.plist +12 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/cli/cli.js +34 -0
- package/dist/cli.js +34 -0
- package/dist/main/index.js +11934 -0
- package/dist/preload/index.js +8 -0
- package/dist/renderer/assets/renderer-2UdJ5Bnz.js +1 -0
- package/dist/renderer/index.html +44 -0
- package/dist/renderer/indexNotFound1.html +44 -0
- package/dist/renderer/indexNotFound2.html +44 -0
- package/docs/DEVSETUP.md +36 -0
- package/electron.vite.config.ts +22 -0
- package/package.json +51 -0
- package/resources/icon.png +0 -0
- package/src/main/index.ts +310 -0
- package/src/main/validateArgs.ts +81 -0
- package/src/main/windows.ts +178 -0
- package/src/preload/index.ts +16 -0
- package/src/renderer/index.html +44 -0
- package/src/renderer/indexNotFound1.html +44 -0
- package/src/renderer/indexNotFound2.html +44 -0
- package/src/renderer/src/renderer.ts +1 -0
- package/tsconfig.json +4 -0
- package/tsconfig.node.json +8 -0
- package/tsconfig.web.json +7 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const electron = require("electron");
|
|
3
|
+
electron.contextBridge.exposeInMainWorld("__HC_ZOME_CALL_SIGNER__", {
|
|
4
|
+
signZomeCall: (zomeCall) => electron.ipcRenderer.invoke("sign-zome-call", zomeCall)
|
|
5
|
+
});
|
|
6
|
+
electron.contextBridge.exposeInMainWorld("electronAPI", {
|
|
7
|
+
signZomeCall: (zomeCall) => electron.ipcRenderer.invoke("sign-zome-call-legacy", zomeCall)
|
|
8
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.error("index.html not found.");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Holochain dev CLI</title>
|
|
6
|
+
<meta
|
|
7
|
+
http-equiv="Content-Security-Policy"
|
|
8
|
+
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
|
|
9
|
+
/>
|
|
10
|
+
<script type="module" crossorigin src="./assets/renderer-2UdJ5Bnz.js"></script>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body>
|
|
14
|
+
<div class="container">
|
|
15
|
+
<h1>index.html not found.</h1>
|
|
16
|
+
<div style="margin-bottom: 100px">
|
|
17
|
+
Is your dev server running at the port specified with the
|
|
18
|
+
<span class="code">--ui-port</span> option?
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
<style>
|
|
24
|
+
body {
|
|
25
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
+
margin: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.container {
|
|
30
|
+
display: flex;
|
|
31
|
+
height: 100vh;
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.code {
|
|
40
|
+
background-color: lightgray;
|
|
41
|
+
border-radius: 3px;
|
|
42
|
+
padding: 1px 3px;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Holochain dev CLI</title>
|
|
6
|
+
<meta
|
|
7
|
+
http-equiv="Content-Security-Policy"
|
|
8
|
+
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
|
|
9
|
+
/>
|
|
10
|
+
<script type="module" crossorigin src="./assets/renderer-2UdJ5Bnz.js"></script>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body>
|
|
14
|
+
<div class="container">
|
|
15
|
+
<h1>index.html not found.</h1>
|
|
16
|
+
<div style="margin-bottom: 100px">
|
|
17
|
+
Make sure the UI assets in your .webhapp file contain an index.html file at the asset
|
|
18
|
+
folder's root level.
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
<style>
|
|
24
|
+
body {
|
|
25
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
+
margin: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.container {
|
|
30
|
+
display: flex;
|
|
31
|
+
height: 100vh;
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.code {
|
|
40
|
+
background-color: lightgray;
|
|
41
|
+
border-radius: 3px;
|
|
42
|
+
padding: 1px 3px;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Holochain dev CLI</title>
|
|
6
|
+
<meta
|
|
7
|
+
http-equiv="Content-Security-Policy"
|
|
8
|
+
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
|
|
9
|
+
/>
|
|
10
|
+
<script type="module" crossorigin src="./assets/renderer-2UdJ5Bnz.js"></script>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body>
|
|
14
|
+
<div class="container">
|
|
15
|
+
<h1>index.html not found.</h1>
|
|
16
|
+
<div style="margin-bottom: 100px">
|
|
17
|
+
Make sure that the path you provided via the <span class="code">--ui-path</span> option
|
|
18
|
+
contains an index.html file.
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
<style>
|
|
24
|
+
body {
|
|
25
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
+
margin: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.container {
|
|
30
|
+
display: flex;
|
|
31
|
+
height: 100vh;
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.code {
|
|
40
|
+
background-color: lightgray;
|
|
41
|
+
border-radius: 3px;
|
|
42
|
+
padding: 1px 3px;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
package/docs/DEVSETUP.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
## Dev Setup
|
|
2
|
+
|
|
3
|
+
To setup the development environment to develop on the CLI itself:
|
|
4
|
+
|
|
5
|
+
1. Install dependencies:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
yarn
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
2. Build Rust node add-ons (requires Rust + Go installed)
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
yarn setup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
3. Run the CLI in development mode
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
yarn dev -- -- [your CLI arguments here]
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
for example
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
yarn dev -- -- --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
to invoke the help menu showing the available CLI arguments and options.
|
|
31
|
+
|
|
32
|
+
4. Building the CLI:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
yarn build
|
|
36
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
main: {
|
|
6
|
+
plugins: [externalizeDepsPlugin({ exclude: ['@holochain/client', 'nanoid', 'get-port'] })],
|
|
7
|
+
},
|
|
8
|
+
preload: {
|
|
9
|
+
plugins: [externalizeDepsPlugin()],
|
|
10
|
+
},
|
|
11
|
+
renderer: {
|
|
12
|
+
build: {
|
|
13
|
+
rollupOptions: {
|
|
14
|
+
input: {
|
|
15
|
+
admin: path.resolve(__dirname, 'src/renderer/index.html'),
|
|
16
|
+
splashscreen: path.resolve(__dirname, 'src/renderer/indexNotFound1.html'),
|
|
17
|
+
selectmediasource: path.resolve(__dirname, 'src/renderer/indexNotFound2.html'),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@holochain/hc-spin",
|
|
3
|
+
"version": "0.100.0",
|
|
4
|
+
"description": "CLI to run Holochain aps during development.",
|
|
5
|
+
"author": "matthme",
|
|
6
|
+
"homepage": "https://developer.holochain.org",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "out/main/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"hc-spin": "./dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
|
15
|
+
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
|
16
|
+
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
|
17
|
+
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
|
18
|
+
"start": "electron-vite preview",
|
|
19
|
+
"dev": "electron-vite dev",
|
|
20
|
+
"build": "rimraf dist && npm run typecheck && electron-vite build && mv ./out ./dist && cp ./cli/cli.js ./dist/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@electron-toolkit/preload": "^3.0.0",
|
|
24
|
+
"@electron-toolkit/utils": "^3.0.0",
|
|
25
|
+
"@holochain/client": "^0.16.3",
|
|
26
|
+
"commander": "11.1.0",
|
|
27
|
+
"electron": "^28.1.1",
|
|
28
|
+
"electron-context-menu": "3.6.1",
|
|
29
|
+
"get-port": "7.0.0",
|
|
30
|
+
"@holochain/hc-spin-rust-utils": "0.100.1",
|
|
31
|
+
"@msgpack/msgpack": "^2.8.0",
|
|
32
|
+
"nanoid": "5.0.4",
|
|
33
|
+
"split": "1.0.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
|
37
|
+
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
|
38
|
+
"@electron-toolkit/tsconfig": "^1.0.1",
|
|
39
|
+
"@types/node": "^18.19.5",
|
|
40
|
+
"bufferutil": "4.0.8",
|
|
41
|
+
"electron-builder": "^24.9.1",
|
|
42
|
+
"electron-vite": "https://github.com/matthme/electron-vite.git#forward-cli-args-dist",
|
|
43
|
+
"eslint": "^8.56.0",
|
|
44
|
+
"prettier": "^3.1.1",
|
|
45
|
+
"rimraf": "5.0.5",
|
|
46
|
+
"typescript": "^5.3.3",
|
|
47
|
+
"utf-8-validate": "^6.0.3",
|
|
48
|
+
"vite": "^5.0.11"
|
|
49
|
+
},
|
|
50
|
+
"packageManager": "yarn@1.22.19"
|
|
51
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { app, IpcMainInvokeEvent, ipcMain, protocol } from 'electron';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { nanoid } from 'nanoid';
|
|
5
|
+
import { Command, Option } from 'commander';
|
|
6
|
+
import contextMenu from 'electron-context-menu';
|
|
7
|
+
import split from 'split';
|
|
8
|
+
import * as childProcess from 'child_process';
|
|
9
|
+
import { ZomeCallNapi, ZomeCallSigner, ZomeCallUnsignedNapi } from '@holochain/hc-spin-rust-utils';
|
|
10
|
+
import { encode } from '@msgpack/msgpack';
|
|
11
|
+
import { createHappWindow } from './windows';
|
|
12
|
+
import getPort from 'get-port';
|
|
13
|
+
import {
|
|
14
|
+
AgentPubKey,
|
|
15
|
+
AppWebsocket,
|
|
16
|
+
CallZomeRequest,
|
|
17
|
+
CallZomeRequestSigned,
|
|
18
|
+
getNonceExpiration,
|
|
19
|
+
randomNonce,
|
|
20
|
+
} from '@holochain/client';
|
|
21
|
+
import { validateCliArgs } from './validateArgs';
|
|
22
|
+
|
|
23
|
+
const rustUtils = require('@holochain/hc-spin-rust-utils');
|
|
24
|
+
|
|
25
|
+
const cli = new Command();
|
|
26
|
+
|
|
27
|
+
cli
|
|
28
|
+
.name('hc-spin')
|
|
29
|
+
.description('CLI to run Holochain apps during development.')
|
|
30
|
+
.version(`0.100.0 (for holochain 0.1.x)`)
|
|
31
|
+
.argument(
|
|
32
|
+
'<path>',
|
|
33
|
+
'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',
|
|
34
|
+
)
|
|
35
|
+
.option(
|
|
36
|
+
'--app-id <string>',
|
|
37
|
+
'Install the app with a specific app id. By default the app id is derived from the name of the .webhapp/.happ file that you pass but this option allows you to set it explicitly',
|
|
38
|
+
)
|
|
39
|
+
.option('--holochain-path <path>', 'Set the path to the holochain binary [default: holochain].')
|
|
40
|
+
.addOption(
|
|
41
|
+
new Option('-n, --num-agents <number>', 'How many agents to spawn the app for.').argParser(
|
|
42
|
+
parseInt,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
.option('--ui-path <path>', "Path to the folder containing the index.html of the webhapp's UI.")
|
|
46
|
+
.option(
|
|
47
|
+
'--ui-port <number>',
|
|
48
|
+
'Port pointing to a localhost dev server that serves your UI assets.',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
cli.parse();
|
|
52
|
+
// console.log('Got CLI opts: ', cli.opts());
|
|
53
|
+
// console.log('Got CLI args: ', cli.args);
|
|
54
|
+
|
|
55
|
+
// In nix shell and on Windows SIGINT does not seem to be emitted so it is read from the command line instead.
|
|
56
|
+
// https://stackoverflow.com/questions/10021373/what-is-the-windows-equivalent-of-process-onsigint-in-node-js
|
|
57
|
+
const rl = require('readline').createInterface({
|
|
58
|
+
input: process.stdin,
|
|
59
|
+
output: process.stdout,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
rl.on('SIGINT', function () {
|
|
63
|
+
process.emit('SIGINT');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
process.on('SIGINT', () => {
|
|
67
|
+
app.quit();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Garbage collect unused directories of previous runs
|
|
71
|
+
const files = fs.readdirSync(app.getPath('temp'));
|
|
72
|
+
const hcSpinFolders = files.filter((file) => file.startsWith(`hc-spin-`));
|
|
73
|
+
for (const folder of hcSpinFolders) {
|
|
74
|
+
const folderPath = path.join(app.getPath('temp'), folder);
|
|
75
|
+
const folderFiles = fs.readdirSync(folderPath);
|
|
76
|
+
if (folderFiles.includes('.abandoned')) {
|
|
77
|
+
fs.rmSync(folderPath, { recursive: true, force: true, maxRetries: 4 });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Set app path to temp directory
|
|
82
|
+
const DATA_ROOT_DIR = path.join(app.getPath('temp'), `hc-spin-${nanoid(8)}`);
|
|
83
|
+
|
|
84
|
+
app.setPath('userData', path.join(DATA_ROOT_DIR, 'electron'));
|
|
85
|
+
|
|
86
|
+
const CLI_OPTS = validateCliArgs(cli.args, cli.opts(), DATA_ROOT_DIR);
|
|
87
|
+
|
|
88
|
+
// const SANDBOX_DIRECTORIES: Array<string> = [];
|
|
89
|
+
const SANDBOX_PROCESSES: childProcess.ChildProcessWithoutNullStreams[] = [];
|
|
90
|
+
const WINDOW_INFO_MAP: Record<
|
|
91
|
+
string,
|
|
92
|
+
{ agentPubKey: AgentPubKey; zomeCallSigner: ZomeCallSigner }
|
|
93
|
+
> = {};
|
|
94
|
+
|
|
95
|
+
protocol.registerSchemesAsPrivileged([
|
|
96
|
+
{
|
|
97
|
+
scheme: 'webhapp',
|
|
98
|
+
privileges: { standard: true },
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
contextMenu({
|
|
103
|
+
showSaveImageAs: true,
|
|
104
|
+
showSearchWithGoogle: false,
|
|
105
|
+
showInspectElement: true,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const handleSignZomeCall = async (e: IpcMainInvokeEvent, request: CallZomeRequest) => {
|
|
109
|
+
const windowInfo = WINDOW_INFO_MAP[e.sender.id];
|
|
110
|
+
if (request.provenance.toString() !== Array.from(windowInfo.agentPubKey).toString())
|
|
111
|
+
return Promise.reject('Agent public key unauthorized.');
|
|
112
|
+
|
|
113
|
+
// console.log("Got zome call request: ", request);
|
|
114
|
+
const zomeCallUnsignedNapi: ZomeCallUnsignedNapi = {
|
|
115
|
+
provenance: Array.from(request.provenance),
|
|
116
|
+
cellId: [Array.from(request.cell_id[0]), Array.from(request.cell_id[1])],
|
|
117
|
+
zomeName: request.zome_name,
|
|
118
|
+
fnName: request.fn_name,
|
|
119
|
+
payload: Array.from(encode(request.payload)),
|
|
120
|
+
nonce: Array.from(await randomNonce()),
|
|
121
|
+
expiresAt: getNonceExpiration(),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const zomeCallSignedNapi: ZomeCallNapi =
|
|
125
|
+
await windowInfo.zomeCallSigner.signZomeCall(zomeCallUnsignedNapi);
|
|
126
|
+
|
|
127
|
+
const zomeCallSigned: CallZomeRequestSigned = {
|
|
128
|
+
provenance: Uint8Array.from(zomeCallSignedNapi.provenance),
|
|
129
|
+
cap_secret: null,
|
|
130
|
+
cell_id: [
|
|
131
|
+
Uint8Array.from(zomeCallSignedNapi.cellId[0]),
|
|
132
|
+
Uint8Array.from(zomeCallSignedNapi.cellId[1]),
|
|
133
|
+
],
|
|
134
|
+
zome_name: zomeCallSignedNapi.zomeName,
|
|
135
|
+
fn_name: zomeCallSignedNapi.fnName,
|
|
136
|
+
payload: Uint8Array.from(zomeCallSignedNapi.payload),
|
|
137
|
+
signature: Uint8Array.from(zomeCallSignedNapi.signature),
|
|
138
|
+
expires_at: zomeCallSignedNapi.expiresAt,
|
|
139
|
+
nonce: Uint8Array.from(zomeCallSignedNapi.nonce),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return zomeCallSigned;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// https://github.com/holochain/holochain-client-js/issues/221
|
|
146
|
+
const handleSignZomeCallLegacy = async (e: IpcMainInvokeEvent, request: ZomeCallUnsignedNapi) => {
|
|
147
|
+
const windowInfo = WINDOW_INFO_MAP[e.sender.id];
|
|
148
|
+
if (request.provenance.toString() !== Array.from(windowInfo.agentPubKey).toString())
|
|
149
|
+
return Promise.reject('Agent public key unauthorized.');
|
|
150
|
+
|
|
151
|
+
return windowInfo.zomeCallSigner.signZomeCall(request);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
async function spawnSandboxes(
|
|
155
|
+
nAgents: number,
|
|
156
|
+
happPath: string,
|
|
157
|
+
appId: string,
|
|
158
|
+
): Promise<[childProcess.ChildProcessWithoutNullStreams, Array<string>, Array<number>]> {
|
|
159
|
+
const generateArgs = [
|
|
160
|
+
'sandbox',
|
|
161
|
+
'--piped',
|
|
162
|
+
'generate',
|
|
163
|
+
'--num-sandboxes',
|
|
164
|
+
nAgents.toString(),
|
|
165
|
+
'--app-id',
|
|
166
|
+
appId,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const appPorts: number[] = [];
|
|
170
|
+
let appPortsString = '';
|
|
171
|
+
for (var i = 1; i <= nAgents; i++) {
|
|
172
|
+
const appPort = await getPort();
|
|
173
|
+
appPortsString += `${appPort},`;
|
|
174
|
+
appPorts.push(appPort);
|
|
175
|
+
}
|
|
176
|
+
generateArgs.push('--run', appPortsString.slice(0, appPortsString.length - 1));
|
|
177
|
+
|
|
178
|
+
// const adminPorts: number[] = [];
|
|
179
|
+
// let adminPortsString = '';
|
|
180
|
+
// for (var i = 1; i <= nAgents; i++) {
|
|
181
|
+
// const adminPort = await getPort();
|
|
182
|
+
// adminPortsString += `${adminPort},`;
|
|
183
|
+
// adminPorts.push(adminPort);
|
|
184
|
+
// }
|
|
185
|
+
// generateArgs.push('--force-admin-ports', adminPortsString.slice(0, adminPortsString.length - 1));
|
|
186
|
+
|
|
187
|
+
generateArgs.push(happPath, 'network', 'mdns');
|
|
188
|
+
// console.log('GENERATE ARGS: ', generateArgs);
|
|
189
|
+
|
|
190
|
+
let readyConductors = 0;
|
|
191
|
+
const sandboxPaths: Array<string> = [];
|
|
192
|
+
|
|
193
|
+
const sandboxHandle = childProcess.spawn('hc', generateArgs);
|
|
194
|
+
sandboxHandle.stdin.write('pass');
|
|
195
|
+
sandboxHandle.stdin.end();
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
sandboxHandle.stdout.pipe(split()).on('data', async (line: string) => {
|
|
198
|
+
console.log(`[hc-spin] | [hc sandbox]: ${line}`);
|
|
199
|
+
if (line.includes('Created directory at:')) {
|
|
200
|
+
// hc-sandbox: Created directory at: /tmp/v7cLY7ls3onZFMmyrFi5y Keep this path to rerun the same sandbox. It has also been saved to a file called `.hc` in your current working directory.
|
|
201
|
+
const sanboxPath = line
|
|
202
|
+
.split('\x1B[1;4;48;5;254;38;5;4m')[1]
|
|
203
|
+
.split('\x1B[0m \x1B[1m')[0]
|
|
204
|
+
.trim();
|
|
205
|
+
|
|
206
|
+
sandboxPaths.push(sanboxPath);
|
|
207
|
+
}
|
|
208
|
+
if (line.includes('Running conductor on admin port')) {
|
|
209
|
+
readyConductors += 1;
|
|
210
|
+
if (readyConductors === nAgents) resolve([sandboxHandle, sandboxPaths, appPorts]);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
sandboxHandle.stderr.pipe(split()).on('data', async (line: string) => {
|
|
214
|
+
console.log(`[hc-spin] | [hc sandbox] ERROR: ${line}`);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// This method will be called when Electron has finished
|
|
220
|
+
// initialization and is ready to create browser windows.
|
|
221
|
+
// Some APIs can only be used after this event occurs.
|
|
222
|
+
app.whenReady().then(async () => {
|
|
223
|
+
ipcMain.handle('sign-zome-call', handleSignZomeCall);
|
|
224
|
+
ipcMain.handle('sign-zome-call-legacy', handleSignZomeCallLegacy);
|
|
225
|
+
|
|
226
|
+
let happTargetDir: string | undefined;
|
|
227
|
+
// TODO unpack assets to UI dir if webhapp is passed
|
|
228
|
+
if (CLI_OPTS.happOrWebhappPath.type === 'webhapp') {
|
|
229
|
+
happTargetDir = path.join(DATA_ROOT_DIR, 'apps', CLI_OPTS.appId);
|
|
230
|
+
const uiTargetDir = path.join(happTargetDir, 'ui');
|
|
231
|
+
await rustUtils.saveHappOrWebhapp(
|
|
232
|
+
CLI_OPTS.happOrWebhappPath.path,
|
|
233
|
+
CLI_OPTS.appId,
|
|
234
|
+
uiTargetDir,
|
|
235
|
+
happTargetDir,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const [sandboxHandle, sandboxPaths, appPorts] = await spawnSandboxes(
|
|
240
|
+
CLI_OPTS.numAgents,
|
|
241
|
+
happTargetDir ? happTargetDir : CLI_OPTS.happOrWebhappPath.path,
|
|
242
|
+
CLI_OPTS.appId,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
console.log('Got app ports: ', appPorts);
|
|
246
|
+
|
|
247
|
+
const lairUrls: string[] = [];
|
|
248
|
+
sandboxPaths.forEach((sandbox) => {
|
|
249
|
+
const conductorConfigPath = path.join(sandbox, 'conductor-config.yaml');
|
|
250
|
+
const configStr = fs.readFileSync(conductorConfigPath, 'utf-8');
|
|
251
|
+
const lines = configStr.split('\n');
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
if (line.includes('connection_url')) {
|
|
254
|
+
// connection_url: unix:///tmp/NgYtyB9jdYSC6BlmNTyra/keystore/socket?k=c-B-bRZIObKsh9c5q899hWjAWsWT28DNQUSElAFLJic
|
|
255
|
+
const lairUrl = line.split('connection_url:')[1].trim();
|
|
256
|
+
lairUrls.push(lairUrl);
|
|
257
|
+
// console.log('Got lairUrl form conductor-config.yaml: ', lairUrl);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
SANDBOX_PROCESSES.push(sandboxHandle);
|
|
264
|
+
|
|
265
|
+
// console.log('Got CLI_OPTS: ', CLI_OPTS);
|
|
266
|
+
|
|
267
|
+
// open browser window for each sandbox
|
|
268
|
+
//
|
|
269
|
+
for (var i = 0; i < cli.opts().numAgents; i++) {
|
|
270
|
+
const zomeCallSigner = await rustUtils.ZomeCallSigner.connect(lairUrls[i], 'pass');
|
|
271
|
+
|
|
272
|
+
const appWs = await AppWebsocket.connect(new URL(`ws://127.0.0.1:${appPorts[i]}`));
|
|
273
|
+
const appInfo = await appWs.appInfo({ installed_app_id: CLI_OPTS.appId });
|
|
274
|
+
const happWindow = await createHappWindow(
|
|
275
|
+
CLI_OPTS.uiSource,
|
|
276
|
+
CLI_OPTS.happOrWebhappPath,
|
|
277
|
+
CLI_OPTS.appId,
|
|
278
|
+
i + 1,
|
|
279
|
+
appPorts[i],
|
|
280
|
+
DATA_ROOT_DIR,
|
|
281
|
+
);
|
|
282
|
+
WINDOW_INFO_MAP[happWindow.webContents.id] = {
|
|
283
|
+
agentPubKey: appInfo.agent_pub_key,
|
|
284
|
+
zomeCallSigner,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// app.on('activate', function () {
|
|
289
|
+
// // On macOS it's common to re-create a window in the app when the
|
|
290
|
+
// // dock icon is clicked and there are no other windows open.
|
|
291
|
+
// if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
292
|
+
// });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Quit when all windows are closed, except on macOS. There, it's common
|
|
296
|
+
// for applications and their menu bar to stay active until the user quits
|
|
297
|
+
// explicitly with Cmd + Q.
|
|
298
|
+
app.on('window-all-closed', () => {
|
|
299
|
+
app.quit();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
app.on('quit', () => {
|
|
303
|
+
fs.writeFileSync(
|
|
304
|
+
path.join(DATA_ROOT_DIR, '.abandoned'),
|
|
305
|
+
"I'm not in use anymore by an active hc-spin process.",
|
|
306
|
+
);
|
|
307
|
+
// clean up sandboxes
|
|
308
|
+
SANDBOX_PROCESSES.forEach((handle) => handle.kill());
|
|
309
|
+
childProcess.spawnSync('hc', ['sandbox', 'clean']);
|
|
310
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { UISource } from './windows';
|
|
4
|
+
|
|
5
|
+
export type CliOpts = {
|
|
6
|
+
appId?: string;
|
|
7
|
+
holochainPath?: string;
|
|
8
|
+
numAgents?: number;
|
|
9
|
+
uiPath?: string;
|
|
10
|
+
uiPort?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type CliOptsValidated = {
|
|
14
|
+
appId: string;
|
|
15
|
+
holochainPath: string | undefined;
|
|
16
|
+
numAgents: number;
|
|
17
|
+
uiSource: UISource;
|
|
18
|
+
happOrWebhappPath: HappOrWebhappPath;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type HappOrWebhappPath = {
|
|
22
|
+
type: 'happ' | 'webhapp';
|
|
23
|
+
path: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function validateCliArgs(
|
|
27
|
+
cliArgs: string[],
|
|
28
|
+
cliOpts: CliOpts,
|
|
29
|
+
appDataRootDir: string,
|
|
30
|
+
): CliOptsValidated {
|
|
31
|
+
if (cliArgs.length !== 1) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`hc spin takes exactly one argument (the path to the .happ or .webhapp file) but got ${cliArgs.length} arguments: ${cliArgs}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const happOrWebhappPath = cliArgs[0];
|
|
37
|
+
if (!happOrWebhappPath.endsWith('.happ') && !happOrWebhappPath.endsWith('.webhapp')) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`The path passed to hc spin must either be a .happ or a .webhapp file but got path '${happOrWebhappPath}'`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (!fs.existsSync(happOrWebhappPath)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Path to .happ or .webhapp file passed as argument does not exist: ${happOrWebhappPath}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (cliOpts.numAgents && typeof cliOpts.numAgents !== 'number') {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`The --num-agents (-n) option must be of type number but got: ${cliOpts.numAgents}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const isHapp = happOrWebhappPath.endsWith('.happ');
|
|
53
|
+
if (isHapp && !cliOpts.uiPath && !cliOpts.uiPort) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'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.',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (cliOpts.uiPath && cliOpts.uiPort) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'Only one of --ui-port and --ui-path is allowed at the same time but got values for both.',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const appId = cliOpts.appId ? cliOpts.appId : path.parse(path.basename(cliArgs[0])).name;
|
|
65
|
+
const holochainPath = cliOpts.holochainPath;
|
|
66
|
+
const numAgents = cliOpts.numAgents ? cliOpts.numAgents : 2;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
appId,
|
|
70
|
+
holochainPath,
|
|
71
|
+
numAgents,
|
|
72
|
+
uiSource: cliOpts.uiPath
|
|
73
|
+
? { type: 'path', path: cliOpts.uiPath }
|
|
74
|
+
: cliOpts.uiPort
|
|
75
|
+
? { type: 'port', port: cliOpts.uiPort }
|
|
76
|
+
: { type: 'path', path: path.join(appDataRootDir, 'apps', appId, 'ui') },
|
|
77
|
+
happOrWebhappPath: isHapp
|
|
78
|
+
? { type: 'happ', path: happOrWebhappPath }
|
|
79
|
+
: { type: 'webhapp', path: happOrWebhappPath },
|
|
80
|
+
};
|
|
81
|
+
}
|