@holochain/hc-spin 0.200.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 +48 -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 +18 -0
- package/dist/cli.js +18 -0
- package/dist/main/index.js +13513 -0
- package/dist/preload/index.js +5 -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/rust-utils/.yarnrc.yml +1 -0
- package/rust-utils/Cargo.toml +44 -0
- package/rust-utils/build.rs +5 -0
- package/rust-utils/index.d.ts +33 -0
- package/rust-utils/index.js +258 -0
- package/rust-utils/package.json +31 -0
- package/rust-utils/src/decode_webhapp.rs +112 -0
- package/rust-utils/src/lib.rs +7 -0
- package/rust-utils/src/types.rs +52 -0
- package/rust-utils/src/utils.rs +4 -0
- package/rust-utils/src/zome_call_signer.rs +99 -0
- package/src/main/index.ts +305 -0
- package/src/main/validateArgs.ts +90 -0
- package/src/main/windows.ts +178 -0
- package/src/preload/index.ts +8 -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,112 @@
|
|
|
1
|
+
use holochain_types::web_app::WebAppBundle;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
#[napi]
|
|
6
|
+
pub async fn save_happ_or_webhapp(
|
|
7
|
+
happ_or_web_happ_path: String,
|
|
8
|
+
app_id: String,
|
|
9
|
+
uis_dir: String,
|
|
10
|
+
happs_dir: String,
|
|
11
|
+
) -> napi::Result<()> {
|
|
12
|
+
let happ_or_webhapp_bytes = fs::read(happ_or_web_happ_path)?;
|
|
13
|
+
|
|
14
|
+
let app_bundle = match WebAppBundle::decode(&happ_or_webhapp_bytes) {
|
|
15
|
+
Ok(web_app_bundle) => {
|
|
16
|
+
// extracting ui.zip bytes
|
|
17
|
+
let web_ui_zip_bytes = web_app_bundle.web_ui_zip_bytes().await.map_err(|e| {
|
|
18
|
+
napi::Error::from_reason(format!("Failed to extract ui zip bytes: {}", e))
|
|
19
|
+
})?;
|
|
20
|
+
|
|
21
|
+
let ui_target_dir = PathBuf::from(uis_dir);
|
|
22
|
+
if !path_exists(&ui_target_dir) {
|
|
23
|
+
fs::create_dir_all(&ui_target_dir)?;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let ui_zip_path = PathBuf::from(ui_target_dir.clone()).join("ui.zip");
|
|
27
|
+
|
|
28
|
+
// unzip and store UI
|
|
29
|
+
fs::write(
|
|
30
|
+
ui_zip_path.clone(),
|
|
31
|
+
web_ui_zip_bytes.into_owned().into_inner(),
|
|
32
|
+
)
|
|
33
|
+
.map_err(|e| {
|
|
34
|
+
napi::Error::from_reason(format!("Failed to write Web UI Zip file: {}", e))
|
|
35
|
+
})?;
|
|
36
|
+
|
|
37
|
+
let file = fs::File::open(ui_zip_path.clone()).map_err(|e| {
|
|
38
|
+
napi::Error::from_reason(format!("Failed to read Web UI Zip file: {}", e))
|
|
39
|
+
})?;
|
|
40
|
+
|
|
41
|
+
unzip_file(file, ui_target_dir.into())
|
|
42
|
+
.map_err(|e| napi::Error::from_reason(format!("Failed to unzip ui.zip: {}", e)))?;
|
|
43
|
+
|
|
44
|
+
fs::remove_file(ui_zip_path).map_err(|e| {
|
|
45
|
+
napi::Error::from_reason(format!("Failed to remove ui.zip after unzipping: {}", e))
|
|
46
|
+
})?;
|
|
47
|
+
|
|
48
|
+
// extracting happ bundle
|
|
49
|
+
let app_bundle = web_app_bundle.happ_bundle().await.map_err(|e| {
|
|
50
|
+
napi::Error::from_reason(format!(
|
|
51
|
+
"Failed to get happ bundle from webapp bundle bytes: {}",
|
|
52
|
+
e
|
|
53
|
+
))
|
|
54
|
+
})?;
|
|
55
|
+
|
|
56
|
+
app_bundle
|
|
57
|
+
}
|
|
58
|
+
Err(e) => {
|
|
59
|
+
return Err(napi::Error::from_reason(format!(
|
|
60
|
+
"Failed to decode .webhapp file: {}",
|
|
61
|
+
e
|
|
62
|
+
)))
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let happs_dir = PathBuf::from(happs_dir);
|
|
67
|
+
if !path_exists(&happs_dir) {
|
|
68
|
+
fs::create_dir_all(&happs_dir)?;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let happ_path = happs_dir.join(format!("{}.happ", app_id));
|
|
72
|
+
|
|
73
|
+
app_bundle
|
|
74
|
+
.write_to_file(&happ_path)
|
|
75
|
+
.await
|
|
76
|
+
.map_err(|e| napi::Error::from_reason(format!("Failed to write .happ file: {}", e)))?;
|
|
77
|
+
|
|
78
|
+
Ok(())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub fn path_exists(path: &PathBuf) -> bool {
|
|
82
|
+
std::path::Path::new(path).exists()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub fn unzip_file(reader: fs::File, outpath: PathBuf) -> Result<(), String> {
|
|
86
|
+
let mut archive = match zip::ZipArchive::new(reader) {
|
|
87
|
+
Ok(a) => a,
|
|
88
|
+
Err(e) => return Err(format!("Failed to unpack zip archive: {}", e)),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for i in 0..archive.len() {
|
|
92
|
+
let mut file = archive.by_index(i).unwrap();
|
|
93
|
+
let outpath = match file.enclosed_name() {
|
|
94
|
+
Some(path) => outpath.join(path).to_owned(),
|
|
95
|
+
None => continue,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (&*file.name()).ends_with('/') {
|
|
99
|
+
fs::create_dir_all(&outpath).unwrap();
|
|
100
|
+
} else {
|
|
101
|
+
if let Some(p) = outpath.parent() {
|
|
102
|
+
if !p.exists() {
|
|
103
|
+
fs::create_dir_all(&p).unwrap();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let mut outfile = fs::File::create(&outpath).unwrap();
|
|
107
|
+
std::io::copy(&mut file, &mut outfile).unwrap();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Ok(())
|
|
112
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
use crate::utils::*;
|
|
2
|
+
use holo_hash::{AgentPubKey, DnaHash};
|
|
3
|
+
use holochain_integrity_types::{FunctionName, ZomeName};
|
|
4
|
+
use holochain_zome_types::CellId;
|
|
5
|
+
use holochain_zome_types::{CapSecret, ExternIO, ZomeCallUnsigned};
|
|
6
|
+
use kitsune_p2p_timestamp::Timestamp;
|
|
7
|
+
|
|
8
|
+
#[derive(Clone)]
|
|
9
|
+
#[napi(object)]
|
|
10
|
+
pub struct ZomeCallUnsignedNapi {
|
|
11
|
+
pub cell_id: Vec<Vec<u8>>,
|
|
12
|
+
pub zome_name: String,
|
|
13
|
+
pub fn_name: String,
|
|
14
|
+
pub payload: Vec<u8>,
|
|
15
|
+
pub cap_secret: Option<Vec<u8>>,
|
|
16
|
+
pub provenance: Vec<u8>,
|
|
17
|
+
pub nonce: Vec<u8>,
|
|
18
|
+
pub expires_at: i64,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl Into<ZomeCallUnsigned> for ZomeCallUnsignedNapi {
|
|
22
|
+
fn into(self: Self) -> ZomeCallUnsigned {
|
|
23
|
+
ZomeCallUnsigned {
|
|
24
|
+
cell_id: CellId::new(
|
|
25
|
+
DnaHash::from_raw_39(self.cell_id.get(0).unwrap().clone()).unwrap(),
|
|
26
|
+
AgentPubKey::from_raw_39(self.cell_id.get(1).unwrap().clone()).unwrap(),
|
|
27
|
+
),
|
|
28
|
+
zome_name: ZomeName::from(self.zome_name),
|
|
29
|
+
fn_name: FunctionName::from(self.fn_name),
|
|
30
|
+
payload: ExternIO::from(self.payload),
|
|
31
|
+
cap_secret: self
|
|
32
|
+
.cap_secret
|
|
33
|
+
.map_or(None, |c| Some(CapSecret::from(vec_to_arr(c)))),
|
|
34
|
+
provenance: AgentPubKey::from_raw_39(self.provenance).unwrap(),
|
|
35
|
+
nonce: vec_to_arr(self.nonce).into(),
|
|
36
|
+
expires_at: Timestamp(self.expires_at),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[napi(object)]
|
|
42
|
+
pub struct ZomeCallNapi {
|
|
43
|
+
pub cell_id: Vec<Vec<u8>>,
|
|
44
|
+
pub zome_name: String,
|
|
45
|
+
pub fn_name: String,
|
|
46
|
+
pub payload: Vec<u8>,
|
|
47
|
+
pub cap_secret: Option<Vec<u8>>,
|
|
48
|
+
pub provenance: Vec<u8>,
|
|
49
|
+
pub nonce: Vec<u8>,
|
|
50
|
+
pub expires_at: i64,
|
|
51
|
+
pub signature: Vec<u8>,
|
|
52
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#![deny(clippy::all)]
|
|
2
|
+
|
|
3
|
+
use std::ops::Deref;
|
|
4
|
+
|
|
5
|
+
use holochain_zome_types::{Signature, ZomeCallUnsigned};
|
|
6
|
+
use lair_keystore_api::{dependencies::url::Url, ipc_keystore::ipc_keystore_connect, LairClient};
|
|
7
|
+
use napi::Result;
|
|
8
|
+
use sodoken::BufRead;
|
|
9
|
+
|
|
10
|
+
use crate::types::*;
|
|
11
|
+
|
|
12
|
+
struct ZomeCallSigner {
|
|
13
|
+
lair_client: LairClient,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl ZomeCallSigner {
|
|
17
|
+
/// Connect to lair keystore
|
|
18
|
+
pub async fn new(connection_url: String, passphrase: String) -> Self {
|
|
19
|
+
let connection_url_parsed = Url::parse(connection_url.deref()).unwrap();
|
|
20
|
+
let passphrase_bufread: BufRead = passphrase.as_bytes().into();
|
|
21
|
+
|
|
22
|
+
let lair_client = ipc_keystore_connect(connection_url_parsed, passphrase_bufread)
|
|
23
|
+
.await
|
|
24
|
+
.unwrap();
|
|
25
|
+
|
|
26
|
+
Self { lair_client }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Sign a zome call
|
|
30
|
+
pub async fn sign_zome_call(
|
|
31
|
+
&self,
|
|
32
|
+
zome_call_unsigned_js: ZomeCallUnsignedNapi,
|
|
33
|
+
) -> Result<ZomeCallNapi> {
|
|
34
|
+
let zome_call_unsigned: ZomeCallUnsigned = zome_call_unsigned_js.clone().into();
|
|
35
|
+
let pub_key = zome_call_unsigned.provenance.clone();
|
|
36
|
+
let mut pub_key_2 = [0; 32];
|
|
37
|
+
pub_key_2.copy_from_slice(pub_key.get_raw_32());
|
|
38
|
+
|
|
39
|
+
let data_to_sign = zome_call_unsigned.data_to_sign().unwrap();
|
|
40
|
+
|
|
41
|
+
let sig = self
|
|
42
|
+
.lair_client
|
|
43
|
+
.sign_by_pub_key(pub_key_2.into(), None, data_to_sign)
|
|
44
|
+
.await
|
|
45
|
+
.unwrap();
|
|
46
|
+
|
|
47
|
+
let signature = Signature(*sig.0);
|
|
48
|
+
|
|
49
|
+
let signed_zome_call = ZomeCallNapi {
|
|
50
|
+
cell_id: zome_call_unsigned_js.cell_id,
|
|
51
|
+
zome_name: zome_call_unsigned.zome_name.to_string(),
|
|
52
|
+
fn_name: zome_call_unsigned.fn_name.0,
|
|
53
|
+
payload: zome_call_unsigned_js.payload,
|
|
54
|
+
cap_secret: zome_call_unsigned_js.cap_secret,
|
|
55
|
+
provenance: zome_call_unsigned_js.provenance,
|
|
56
|
+
nonce: zome_call_unsigned_js.nonce,
|
|
57
|
+
expires_at: zome_call_unsigned_js.expires_at,
|
|
58
|
+
signature: signature.0.to_vec(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
Ok(signed_zome_call)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[napi(js_name = "ZomeCallSigner")]
|
|
66
|
+
pub struct JsZomeCallSigner {
|
|
67
|
+
zome_call_signer: Option<ZomeCallSigner>,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[napi]
|
|
71
|
+
impl JsZomeCallSigner {
|
|
72
|
+
#[napi(constructor)]
|
|
73
|
+
pub fn new() -> Self {
|
|
74
|
+
Self {
|
|
75
|
+
zome_call_signer: None,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[napi]
|
|
80
|
+
pub async fn connect(connection_url: String, passphrase: String) -> Self {
|
|
81
|
+
let zome_call_signer = ZomeCallSigner::new(connection_url, passphrase).await;
|
|
82
|
+
|
|
83
|
+
JsZomeCallSigner {
|
|
84
|
+
zome_call_signer: Some(zome_call_signer),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#[napi]
|
|
89
|
+
pub async fn sign_zome_call(
|
|
90
|
+
&self,
|
|
91
|
+
zome_call_unsigned_js: ZomeCallUnsignedNapi,
|
|
92
|
+
) -> Result<ZomeCallNapi> {
|
|
93
|
+
self.zome_call_signer
|
|
94
|
+
.as_ref()
|
|
95
|
+
.unwrap()
|
|
96
|
+
.sign_zome_call(zome_call_unsigned_js)
|
|
97
|
+
.await
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
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 { ZomeCallSigner, ZomeCallUnsignedNapi } from 'hc-spin-rust-utils';
|
|
10
|
+
import { createHappWindow } from './windows';
|
|
11
|
+
import getPort from 'get-port';
|
|
12
|
+
import { AgentPubKey, AppWebsocket } from '@holochain/client';
|
|
13
|
+
import { validateCliArgs } from './validateArgs';
|
|
14
|
+
|
|
15
|
+
const rustUtils = require('hc-spin-rust-utils');
|
|
16
|
+
|
|
17
|
+
const cli = new Command();
|
|
18
|
+
|
|
19
|
+
cli
|
|
20
|
+
.name('hc-spin')
|
|
21
|
+
.description('CLI to run Holochain aps during development.')
|
|
22
|
+
.version(`${app.getVersion()} (for holochain 0.2.x)`)
|
|
23
|
+
.argument(
|
|
24
|
+
'<path>',
|
|
25
|
+
'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',
|
|
26
|
+
)
|
|
27
|
+
.option(
|
|
28
|
+
'--app-id <string>',
|
|
29
|
+
'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',
|
|
30
|
+
)
|
|
31
|
+
.option(
|
|
32
|
+
'--bootstrap-url <url>',
|
|
33
|
+
'Url of the bootstrap server to use. By default, hc spin spins up a local development bootstrap server for you but this argument allows you to specify a custom one.',
|
|
34
|
+
)
|
|
35
|
+
.option('--holochain-path <path>', 'Set the path to the holochain binary [default: holochain].')
|
|
36
|
+
.addOption(
|
|
37
|
+
new Option('-n, --num-agents <number>', 'How many agents to spawn the app for.').argParser(
|
|
38
|
+
parseInt,
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
.option('--network-seed <string>', 'Install the app with a specific network seed.')
|
|
42
|
+
.option('--ui-path <path>', "Path to the folder containing the index.html of the webhapp's UI.")
|
|
43
|
+
.option(
|
|
44
|
+
'--ui-port <number>',
|
|
45
|
+
'Port pointing to a localhost dev server that serves your UI assets.',
|
|
46
|
+
)
|
|
47
|
+
.option(
|
|
48
|
+
'--signaling-url <url>',
|
|
49
|
+
'Url of the signaling server to use. By default, hc spin spins up a local development signaling server for you but this argument allows you to specify a custom one.',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
cli.parse();
|
|
53
|
+
// console.log('Got CLI opts: ', cli.opts());
|
|
54
|
+
// console.log('Got CLI args: ', cli.args);
|
|
55
|
+
|
|
56
|
+
// Garbage collect unused directories of previous runs
|
|
57
|
+
const files = fs.readdirSync(app.getPath('temp'));
|
|
58
|
+
const hcSpinFolders = files.filter((file) => file.startsWith(`hc-spin-`));
|
|
59
|
+
for (const folder of hcSpinFolders) {
|
|
60
|
+
const folderPath = path.join(app.getPath('temp'), folder);
|
|
61
|
+
const folderFiles = fs.readdirSync(folderPath);
|
|
62
|
+
if (folderFiles.includes('.abandoned')) {
|
|
63
|
+
fs.rmSync(folderPath, { recursive: true, force: true, maxRetries: 4 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Set app path to temp directory
|
|
68
|
+
const DATA_ROOT_DIR = path.join(app.getPath('temp'), `hc-spin-${nanoid(8)}`);
|
|
69
|
+
|
|
70
|
+
app.setPath('userData', path.join(DATA_ROOT_DIR, 'electron'));
|
|
71
|
+
|
|
72
|
+
const CLI_OPTS = validateCliArgs(cli.args, cli.opts(), DATA_ROOT_DIR);
|
|
73
|
+
|
|
74
|
+
// const SANDBOX_DIRECTORIES: Array<string> = [];
|
|
75
|
+
const SANDBOX_PROCESSES: childProcess.ChildProcessWithoutNullStreams[] = [];
|
|
76
|
+
const WINDOW_INFO_MAP: Record<
|
|
77
|
+
string,
|
|
78
|
+
{ agentPubKey: AgentPubKey; zomeCallSigner: ZomeCallSigner }
|
|
79
|
+
> = {};
|
|
80
|
+
|
|
81
|
+
protocol.registerSchemesAsPrivileged([
|
|
82
|
+
{
|
|
83
|
+
scheme: 'webhapp',
|
|
84
|
+
privileges: { standard: true },
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
contextMenu({
|
|
89
|
+
showSaveImageAs: true,
|
|
90
|
+
showSearchWithGoogle: false,
|
|
91
|
+
showInspectElement: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const handleSignZomeCall = (e: IpcMainInvokeEvent, zomeCall: ZomeCallUnsignedNapi) => {
|
|
95
|
+
const windowInfo = WINDOW_INFO_MAP[e.sender.id];
|
|
96
|
+
if (zomeCall.provenance.toString() !== Array.from(windowInfo.agentPubKey).toString())
|
|
97
|
+
return Promise.reject('Agent public key unauthorized.');
|
|
98
|
+
return windowInfo.zomeCallSigner.signZomeCall(zomeCall);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
async function startLocalServices(): Promise<[string, string]> {
|
|
102
|
+
const localServicesHandle = childProcess.spawn('hc', ['run-local-services']);
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
let bootStrapUrl;
|
|
105
|
+
let signalUrl;
|
|
106
|
+
let bootstrapRunning = false;
|
|
107
|
+
let signalRunnig = false;
|
|
108
|
+
localServicesHandle.stdout.pipe(split()).on('data', async (line: string) => {
|
|
109
|
+
console.log(`[hc-spin] | [hc run-local-services]: ${line}`);
|
|
110
|
+
if (line.includes('HC BOOTSTRAP - ADDR:')) {
|
|
111
|
+
bootStrapUrl = line.split('# HC BOOTSTRAP - ADDR:')[1].trim();
|
|
112
|
+
}
|
|
113
|
+
if (line.includes('HC SIGNAL - ADDR:')) {
|
|
114
|
+
signalUrl = line.split('# HC SIGNAL - ADDR:')[1].trim();
|
|
115
|
+
}
|
|
116
|
+
if (line.includes('HC BOOTSTRAP - RUNNING')) {
|
|
117
|
+
bootstrapRunning = true;
|
|
118
|
+
}
|
|
119
|
+
if (line.includes('HC SIGNAL - RUNNING')) {
|
|
120
|
+
signalRunnig = true;
|
|
121
|
+
}
|
|
122
|
+
if (bootstrapRunning && signalRunnig) resolve([bootStrapUrl, signalUrl]);
|
|
123
|
+
});
|
|
124
|
+
localServicesHandle.stderr.pipe(split()).on('data', async (line: string) => {
|
|
125
|
+
console.log(`[hc-spin] | [hc run-local-services] ERROR: ${line}`);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type PortsInfo = {
|
|
131
|
+
admin_port: number;
|
|
132
|
+
app_ports: number[];
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
async function spawnSandboxes(
|
|
136
|
+
nAgents: number,
|
|
137
|
+
happPath: string,
|
|
138
|
+
bootStrapUrl: string,
|
|
139
|
+
signalUrl: string,
|
|
140
|
+
appId: string,
|
|
141
|
+
networkSeed?: string,
|
|
142
|
+
): Promise<
|
|
143
|
+
[childProcess.ChildProcessWithoutNullStreams, Array<string>, Record<number, PortsInfo>]
|
|
144
|
+
> {
|
|
145
|
+
const generateArgs = [
|
|
146
|
+
'sandbox',
|
|
147
|
+
'--piped',
|
|
148
|
+
'generate',
|
|
149
|
+
'--num-sandboxes',
|
|
150
|
+
nAgents.toString(),
|
|
151
|
+
'--app-id',
|
|
152
|
+
appId,
|
|
153
|
+
'--run',
|
|
154
|
+
];
|
|
155
|
+
let appPorts = '';
|
|
156
|
+
for (var i = 1; i <= nAgents; i++) {
|
|
157
|
+
const appPort = await getPort();
|
|
158
|
+
appPorts += `${appPort},`;
|
|
159
|
+
}
|
|
160
|
+
generateArgs.push(appPorts.slice(0, appPorts.length - 1));
|
|
161
|
+
|
|
162
|
+
if (networkSeed) {
|
|
163
|
+
generateArgs.push('--network-seed');
|
|
164
|
+
generateArgs.push(networkSeed);
|
|
165
|
+
}
|
|
166
|
+
generateArgs.push(happPath, 'network', '--bootstrap', bootStrapUrl, 'webrtc', signalUrl);
|
|
167
|
+
// console.log('GENERATE ARGS: ', generateArgs);
|
|
168
|
+
|
|
169
|
+
let readyConductors = 0;
|
|
170
|
+
const portsInfo: Record<number, PortsInfo> = {};
|
|
171
|
+
const sandboxPaths: Array<string> = [];
|
|
172
|
+
const lairUrls: string[] = [];
|
|
173
|
+
|
|
174
|
+
const sandboxHandle = childProcess.spawn('hc', generateArgs);
|
|
175
|
+
sandboxHandle.stdin.write('pass');
|
|
176
|
+
sandboxHandle.stdin.end();
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
sandboxHandle.stdout.pipe(split()).on('data', async (line: string) => {
|
|
179
|
+
console.log(`[hc-spin] | [hc sandbox]: ${line}`);
|
|
180
|
+
if (line.includes('Created directory at:')) {
|
|
181
|
+
// 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.
|
|
182
|
+
const sanboxPath = line
|
|
183
|
+
.split('\x1B[1;4;48;5;254;38;5;4m')[1]
|
|
184
|
+
.split('\x1B[0m \x1B[1m')[0]
|
|
185
|
+
.trim();
|
|
186
|
+
|
|
187
|
+
sandboxPaths.push(sanboxPath);
|
|
188
|
+
}
|
|
189
|
+
if (line.includes('lair-keystore connection_url')) {
|
|
190
|
+
const lairKeystoreUrl = line.split('#')[2].trim();
|
|
191
|
+
lairUrls.push(lairKeystoreUrl);
|
|
192
|
+
}
|
|
193
|
+
if (line.includes('Conductor launched')) {
|
|
194
|
+
// hc-sandbox: Conductor launched #!1 {"admin_port":37045,"app_ports":[]}
|
|
195
|
+
const split1 = line.split('{');
|
|
196
|
+
const ports: PortsInfo = JSON.parse(`{${split1[1]}`);
|
|
197
|
+
const conductorNum = split1[0].split('#!')[1].trim();
|
|
198
|
+
portsInfo[conductorNum] = ports;
|
|
199
|
+
// hc-sandbox: Conductor launched #!1 {"admin_port":32805,"app_ports":[45309]}
|
|
200
|
+
readyConductors += 1;
|
|
201
|
+
if (readyConductors === nAgents) resolve([sandboxHandle, sandboxPaths, portsInfo]);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
sandboxHandle.stderr.pipe(split()).on('data', async (line: string) => {
|
|
205
|
+
console.log(`[hc-spin] | [hc sandbox] ERROR: ${line}`);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// This method will be called when Electron has finished
|
|
211
|
+
// initialization and is ready to create browser windows.
|
|
212
|
+
// Some APIs can only be used after this event occurs.
|
|
213
|
+
app.whenReady().then(async () => {
|
|
214
|
+
ipcMain.handle('sign-zome-call', handleSignZomeCall);
|
|
215
|
+
|
|
216
|
+
let happTargetDir: string | undefined;
|
|
217
|
+
// TODO unpack assets to UI dir if webhapp is passed
|
|
218
|
+
if (CLI_OPTS.happOrWebhappPath.type === 'webhapp') {
|
|
219
|
+
happTargetDir = path.join(DATA_ROOT_DIR, 'apps', CLI_OPTS.appId);
|
|
220
|
+
const uiTargetDir = path.join(happTargetDir, 'ui');
|
|
221
|
+
await rustUtils.saveHappOrWebhapp(
|
|
222
|
+
CLI_OPTS.happOrWebhappPath.path,
|
|
223
|
+
CLI_OPTS.appId,
|
|
224
|
+
uiTargetDir,
|
|
225
|
+
happTargetDir,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const [bootstrapUrl, signalingUrl] = await startLocalServices();
|
|
230
|
+
|
|
231
|
+
const [sandboxHandle, sandboxPaths, portsInfo] = await spawnSandboxes(
|
|
232
|
+
CLI_OPTS.numAgents,
|
|
233
|
+
happTargetDir ? happTargetDir : CLI_OPTS.happOrWebhappPath.path,
|
|
234
|
+
CLI_OPTS.bootstrapUrl ? CLI_OPTS.bootstrapUrl : bootstrapUrl,
|
|
235
|
+
CLI_OPTS.singalingUrl ? CLI_OPTS.singalingUrl : signalingUrl,
|
|
236
|
+
CLI_OPTS.appId,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const lairUrls: string[] = [];
|
|
240
|
+
sandboxPaths.forEach((sandbox) => {
|
|
241
|
+
const conductorConfigPath = path.join(sandbox, 'conductor-config.yaml');
|
|
242
|
+
const configStr = fs.readFileSync(conductorConfigPath, 'utf-8');
|
|
243
|
+
const lines = configStr.split('\n');
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
if (line.includes('connection_url')) {
|
|
246
|
+
// connection_url: unix:///tmp/NgYtyB9jdYSC6BlmNTyra/keystore/socket?k=c-B-bRZIObKsh9c5q899hWjAWsWT28DNQUSElAFLJic
|
|
247
|
+
const lairUrl = line.split('connection_url:')[1].trim();
|
|
248
|
+
lairUrls.push(lairUrl);
|
|
249
|
+
// console.log('Got lairUrl form conductor-config.yaml: ', lairUrl);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
SANDBOX_PROCESSES.push(sandboxHandle);
|
|
256
|
+
|
|
257
|
+
// console.log('Got CLI_OPTS: ', CLI_OPTS);
|
|
258
|
+
|
|
259
|
+
// open browser window for each sandbox
|
|
260
|
+
//
|
|
261
|
+
for (var i = 0; i < cli.opts().numAgents; i++) {
|
|
262
|
+
const zomeCallSigner = await rustUtils.ZomeCallSigner.connect(lairUrls[i], 'pass');
|
|
263
|
+
|
|
264
|
+
const appPort = portsInfo[i].app_ports[0];
|
|
265
|
+
const appWs = await AppWebsocket.connect(new URL(`ws://127.0.0.1:${appPort}`));
|
|
266
|
+
const appInfo = await appWs.appInfo({ installed_app_id: CLI_OPTS.appId });
|
|
267
|
+
const happWindow = await createHappWindow(
|
|
268
|
+
CLI_OPTS.uiSource,
|
|
269
|
+
CLI_OPTS.happOrWebhappPath,
|
|
270
|
+
CLI_OPTS.appId,
|
|
271
|
+
i + 1,
|
|
272
|
+
appPort,
|
|
273
|
+
DATA_ROOT_DIR,
|
|
274
|
+
);
|
|
275
|
+
WINDOW_INFO_MAP[happWindow.webContents.id] = {
|
|
276
|
+
agentPubKey: appInfo.agent_pub_key,
|
|
277
|
+
zomeCallSigner,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// app.on('activate', function () {
|
|
282
|
+
// // On macOS it's common to re-create a window in the app when the
|
|
283
|
+
// // dock icon is clicked and there are no other windows open.
|
|
284
|
+
// if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
285
|
+
// });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Quit when all windows are closed, except on macOS. There, it's common
|
|
289
|
+
// for applications and their menu bar to stay active until the user quits
|
|
290
|
+
// explicitly with Cmd + Q.
|
|
291
|
+
app.on('window-all-closed', () => {
|
|
292
|
+
if (process.platform !== 'darwin') {
|
|
293
|
+
app.quit();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
app.on('quit', () => {
|
|
298
|
+
fs.writeFileSync(
|
|
299
|
+
path.join(DATA_ROOT_DIR, '.abandoned'),
|
|
300
|
+
"I'm not in use anymore by an active hc-spin process.",
|
|
301
|
+
);
|
|
302
|
+
// clean up sandboxes
|
|
303
|
+
SANDBOX_PROCESSES.forEach((handle) => handle.kill());
|
|
304
|
+
childProcess.spawnSync('hc', ['sandbox', 'clean']);
|
|
305
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
networkSeed?: string;
|
|
10
|
+
uiPath?: string;
|
|
11
|
+
uiPort?: number;
|
|
12
|
+
signalingUrl?: string;
|
|
13
|
+
bootstrapUrl?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CliOptsValidated = {
|
|
17
|
+
appId: string;
|
|
18
|
+
holochainPath: string | undefined;
|
|
19
|
+
numAgents: number;
|
|
20
|
+
networkSeed: string | undefined;
|
|
21
|
+
uiSource: UISource;
|
|
22
|
+
singalingUrl: string | undefined;
|
|
23
|
+
bootstrapUrl: string | undefined;
|
|
24
|
+
happOrWebhappPath: HappOrWebhappPath;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type HappOrWebhappPath = {
|
|
28
|
+
type: 'happ' | 'webhapp';
|
|
29
|
+
path: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function validateCliArgs(
|
|
33
|
+
cliArgs: string[],
|
|
34
|
+
cliOpts: CliOpts,
|
|
35
|
+
appDataRootDir: string,
|
|
36
|
+
): CliOptsValidated {
|
|
37
|
+
if (cliArgs.length !== 1) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`hc spin takes exactly one argument (the path to the .happ or .webhapp file) but got ${cliArgs.length} arguments: ${cliArgs}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const happOrWebhappPath = cliArgs[0];
|
|
43
|
+
if (!happOrWebhappPath.endsWith('.happ') && !happOrWebhappPath.endsWith('.webhapp')) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`The path passed to hc spin must either be a .happ or a .webhapp file but got path '${happOrWebhappPath}'`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (!fs.existsSync(happOrWebhappPath)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Path to .happ or .webhapp file passed as argument does not exist: ${happOrWebhappPath}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (cliOpts.numAgents && typeof cliOpts.numAgents !== 'number') {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`The --num-agents (-n) option must be of type number but got: ${cliOpts.numAgents}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const isHapp = happOrWebhappPath.endsWith('.happ');
|
|
59
|
+
if (isHapp && !cliOpts.uiPath && !cliOpts.uiPort) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'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.',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (cliOpts.uiPath && cliOpts.uiPort) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'Only one of --ui-port and --ui-path is allowed at the same time but got values for both.',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const appId = cliOpts.appId ? cliOpts.appId : path.parse(path.basename(cliArgs[0])).name;
|
|
71
|
+
const holochainPath = cliOpts.holochainPath;
|
|
72
|
+
const numAgents = cliOpts.numAgents ? cliOpts.numAgents : 2;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
appId,
|
|
76
|
+
holochainPath,
|
|
77
|
+
numAgents,
|
|
78
|
+
networkSeed: cliOpts.networkSeed,
|
|
79
|
+
uiSource: cliOpts.uiPath
|
|
80
|
+
? { type: 'path', path: cliOpts.uiPath }
|
|
81
|
+
: cliOpts.uiPort
|
|
82
|
+
? { type: 'port', port: cliOpts.uiPort }
|
|
83
|
+
: { type: 'path', path: path.join(appDataRootDir, 'apps', appId, 'ui') },
|
|
84
|
+
singalingUrl: cliOpts.signalingUrl,
|
|
85
|
+
bootstrapUrl: cliOpts.bootstrapUrl,
|
|
86
|
+
happOrWebhappPath: isHapp
|
|
87
|
+
? { type: 'happ', path: happOrWebhappPath }
|
|
88
|
+
: { type: 'webhapp', path: happOrWebhappPath },
|
|
89
|
+
};
|
|
90
|
+
}
|