Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logic to deal with interruptions at intermediate states during key generation and setup #225

Merged
merged 4 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 36 additions & 34 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ contextMenu({
click: () => (browserWindow as BrowserWindow).reload(),
},
{
label: 'Quit',
label: 'Quit Launcher',
click: () => app.quit(),
},
],
Expand All @@ -185,14 +185,14 @@ if (!isFirstInstance && app.isPackaged && !VALIDATED_CLI_ARGS.profile) {
}

app.on('second-instance', () => {
if (PRIVILEDGED_LAUNCHER_WINDOWS[MAIN_WINDOW]) {
PRIVILEDGED_LAUNCHER_WINDOWS[MAIN_WINDOW].show();
if (PRIVILEGED_LAUNCHER_WINDOWS[MAIN_WINDOW]) {
PRIVILEGED_LAUNCHER_WINDOWS[MAIN_WINDOW].show();
}
});

app.on('activate', () => {
if (PRIVILEDGED_LAUNCHER_WINDOWS[MAIN_WINDOW]) {
PRIVILEDGED_LAUNCHER_WINDOWS[MAIN_WINDOW].show();
if (PRIVILEGED_LAUNCHER_WINDOWS[MAIN_WINDOW]) {
PRIVILEGED_LAUNCHER_WINDOWS[MAIN_WINDOW].show();
}
});

Expand Down Expand Up @@ -228,7 +228,7 @@ let DEFAULT_HOLOCHAIN_DATA_ROOT: HolochainDataRoot | undefined;
const HOLOCHAIN_MANAGERS: Record<string, HolochainManager> = {}; // holochain managers sorted by HolochainDataRoot.name
let LAIR_HANDLE: ChildProcessWithoutNullStreams | undefined;
let LAIR_URL: string | undefined;
let PRIVILEDGED_LAUNCHER_WINDOWS: Record<AdminWindow, BrowserWindow>; // Admin windows with special zome call signing priviledges
let PRIVILEGED_LAUNCHER_WINDOWS: Record<AdminWindow, BrowserWindow>; // Admin windows with special zome call signing priviledges
const WINDOW_INFO_MAP: WindowInfoRecord = {}; // WindowInfo by webContents.id - used to verify origin of zome call requests
let IS_LAUNCHED = false;
let IS_QUITTING = false;
Expand All @@ -250,10 +250,10 @@ app.whenReady().then(async () => {

console.log('BEING RUN IN __dirnmane: ', __dirname);

PRIVILEDGED_LAUNCHER_WINDOWS = setupAppWindows(LAUNCHER_EMITTER);
PRIVILEGED_LAUNCHER_WINDOWS = setupAppWindows(LAUNCHER_EMITTER);

const mainWindow = PRIVILEDGED_LAUNCHER_WINDOWS[MAIN_WINDOW];
const settingsWindow = PRIVILEDGED_LAUNCHER_WINDOWS[SETTINGS_WINDOW];
const mainWindow = PRIVILEGED_LAUNCHER_WINDOWS[MAIN_WINDOW];
const settingsWindow = PRIVILEGED_LAUNCHER_WINDOWS[SETTINGS_WINDOW];

mainWindow.on('close', (e) => {
if (IS_QUITTING) return;
Expand All @@ -279,13 +279,13 @@ app.whenReady().then(async () => {
});

// make sure window objects get deleted after closing
Object.entries(PRIVILEDGED_LAUNCHER_WINDOWS).forEach(([key, window]) => {
Object.entries(PRIVILEGED_LAUNCHER_WINDOWS).forEach(([key, window]) => {
window.on('closed', () => {
delete PRIVILEDGED_LAUNCHER_WINDOWS[key];
delete PRIVILEGED_LAUNCHER_WINDOWS[key];
});
});

createIPCHandler({ router, windows: Object.values(PRIVILEDGED_LAUNCHER_WINDOWS) });
createIPCHandler({ router, windows: Object.values(PRIVILEGED_LAUNCHER_WINDOWS) });

ipcMain.handle('sign-zome-call', handleSignZomeCall);

Expand Down Expand Up @@ -371,7 +371,7 @@ const handleSignZomeCall = async (e: IpcMainInvokeEvent, request: CallZomeReques
} else {
// Launcher admin windows get a wildcard to have any zome calls signed
if (
Object.values(PRIVILEDGED_LAUNCHER_WINDOWS)
Object.values(PRIVILEGED_LAUNCHER_WINDOWS)
.map((window) => window.webContents.id)
.includes(e.sender.id)
) {
Expand All @@ -391,20 +391,15 @@ const handleSignZomeCall = async (e: IpcMainInvokeEvent, request: CallZomeReques
};

// Kills any pre-existing lair processes and start up lair and assigns associated global variables
async function killIfExistsAndLaunchLair(password: string): Promise<void> {
async function launchLairIfNecessary(password: string): Promise<void> {
INTEGRITY_CHECKER = new IntegrityChecker(password);
LAUNCHER_FILE_SYSTEM.setIntegrityChecker(INTEGRITY_CHECKER);
LAUNCHER_EMITTER.emit(LOADING_PROGRESS_UPDATE, 'startingLairKeystore');

if (LAIR_HANDLE) {
LAIR_HANDLE.kill();
LAIR_HANDLE = undefined;
}

if (VALIDATED_CLI_ARGS.holochainVersion.type === 'running-external') {
LAIR_URL = VALIDATED_CLI_ARGS.holochainVersion.lairUrl;
DEFAULT_LAIR_CLIENT = await rustUtils.LauncherLairClient.connect(LAIR_URL, password);
} else {
} else if (!LAIR_HANDLE) {
const [lairHandle, lairUrl2] = await launchLairKeystore(
VALIDATED_CLI_ARGS.lairBinaryPath,
LAUNCHER_FILE_SYSTEM.keystoreDir,
Expand All @@ -424,8 +419,7 @@ async function killIfExistsAndLaunchLair(password: string): Promise<void> {
* root/device seeds and revocation keys
*/
async function handleQuickSetup(password: string): Promise<void> {
if (!PRIVILEDGED_LAUNCHER_WINDOWS)
throw new Error('Main window needs to exist before launching.');
if (!PRIVILEGED_LAUNCHER_WINDOWS) throw new Error('Main window needs to exist before launching.');
const lairHandleTemp = spawnSync(VALIDATED_CLI_ARGS.lairBinaryPath, ['--version']);
if (!lairHandleTemp.stdout) {
console.error(`Failed to run lair-keystore binary:\n${lairHandleTemp}`);
Expand All @@ -444,7 +438,7 @@ async function handleQuickSetup(password: string): Promise<void> {
}

// 2. Start up lair keystore process and connect to it
await killIfExistsAndLaunchLair(password);
await launchLairIfNecessary(password);

// 3. Generate key recovery file and import device seed if necessary
const dpkiDeviceSeedExists = await DEFAULT_LAIR_CLIENT!.seedExists(DEVICE_SEED_LAIR_TAG);
Expand Down Expand Up @@ -495,7 +489,7 @@ async function handleAdvancedSetup(
}

// 2. Start up lair keystore process and connect to it
await killIfExistsAndLaunchLair(password);
await launchLairIfNecessary(password);

// 3. Make sure that there is no DPKI_DEVICE_SEED in lair yet
const dpkiDeviceSeedExists = await DEFAULT_LAIR_CLIENT!.seedExists(DEVICE_SEED_LAIR_TAG);
Expand Down Expand Up @@ -531,8 +525,7 @@ async function launchHolochain(password: string, lairUrl: string): Promise<void>

LAUNCHER_EMITTER.emit(LOADING_PROGRESS_UPDATE, 'startingHolochain');

if (!PRIVILEDGED_LAUNCHER_WINDOWS)
throw new Error('Main window needs to exist before launching.');
if (!PRIVILEGED_LAUNCHER_WINDOWS) throw new Error('Main window needs to exist before launching.');

const nonDefaultPartition: HolochainPartition =
VALIDATED_CLI_ARGS.holochainVersion.type === 'running-external'
Expand Down Expand Up @@ -632,15 +625,15 @@ const getDevhubAppClient = async () => {

const router = t.router({
openSettings: t.procedure.mutation(() => {
const mainWindow = PRIVILEDGED_LAUNCHER_WINDOWS[MAIN_WINDOW];
const mainWindow = PRIVILEGED_LAUNCHER_WINDOWS[MAIN_WINDOW];
const [xMain, yMain] = mainWindow.getPosition();
const settingsWindow = PRIVILEDGED_LAUNCHER_WINDOWS[SETTINGS_WINDOW];
const settingsWindow = PRIVILEGED_LAUNCHER_WINDOWS[SETTINGS_WINDOW];
if (!settingsWindow) throw new Error('Settings window is undefined.');
settingsWindow.setPosition(xMain + 50, yMain - 50);
settingsWindow.show();
}),
hideApp: t.procedure.mutation(() => {
PRIVILEDGED_LAUNCHER_WINDOWS[MAIN_WINDOW].hide();
PRIVILEGED_LAUNCHER_WINDOWS[MAIN_WINDOW].hide();
}),
openApp: t.procedure.input(ExtendedAppInfoSchema).mutation(async (opts) => {
const { appInfo, holochainDataRoot } = opts.input;
Expand Down Expand Up @@ -882,6 +875,15 @@ const router = t.router({

return !isInitializedValidated;
}),
dpkiDeviceSeedPresent: t.procedure
.input(z.object({ password: z.string() }))
.mutation(async (opts) => {
const password = opts.input.password;
if (!DEFAULT_LAIR_CLIENT || !LAIR_URL || !LAIR_HANDLE) {
await launchLairIfNecessary(password);
}
return DEFAULT_LAIR_CLIENT!.seedExists(DEVICE_SEED_LAIR_TAG);
}),
getHolochainStorageInfo: t.procedure.query(() => {
const defaultHolochainManager = HOLOCHAIN_MANAGERS[BREAKING_DEFAULT_HOLOCHAIN_VERSION];
const holochainDataRoot = defaultHolochainManager.holochainDataRoot;
Expand Down Expand Up @@ -950,15 +952,15 @@ const router = t.router({
}),
launch: t.procedure.input(z.object({ password: z.string() })).mutation(async (opts) => {
const password = opts.input.password;
if (!PRIVILEDGED_LAUNCHER_WINDOWS)
if (!PRIVILEGED_LAUNCHER_WINDOWS)
throw new Error('Main window needs to exist before launching.');
if (!DEFAULT_LAIR_CLIENT || !LAIR_URL || !LAIR_HANDLE) {
await killIfExistsAndLaunchLair(password);
await launchLairIfNecessary(password);
}
await launchHolochain(password, LAIR_URL!);
IS_LAUNCHED = true;
PRIVILEDGED_LAUNCHER_WINDOWS[MAIN_WINDOW].setSize(WINDOW_SIZE, MIN_HEIGHT, true);
loadOrServe(PRIVILEDGED_LAUNCHER_WINDOWS[SETTINGS_WINDOW], { screen: SETTINGS_WINDOW });
PRIVILEGED_LAUNCHER_WINDOWS[MAIN_WINDOW].setSize(WINDOW_SIZE, MIN_HEIGHT, true);
loadOrServe(PRIVILEGED_LAUNCHER_WINDOWS[SETTINGS_WINDOW], { screen: SETTINGS_WINDOW });
}),
initializeDefaultAppPorts: t.procedure.query(async () => {
const defaultHolochainManager = HOLOCHAIN_MANAGERS[DEFAULT_HOLOCHAIN_DATA_ROOT!.name];
Expand Down Expand Up @@ -1037,7 +1039,7 @@ const router = t.router({
factoryResetUtility({
launcherFileSystem: LAUNCHER_FILE_SYSTEM,
windowInfoMap: WINDOW_INFO_MAP,
privilegedLauncherWindows: PRIVILEDGED_LAUNCHER_WINDOWS,
privilegedLauncherWindows: PRIVILEGED_LAUNCHER_WINDOWS,
holochainManagers: HOLOCHAIN_MANAGERS,
lairHandle: LAIR_HANDLE,
app,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ body {
}

.btn-black {
@apply btn px-2 border border-surface-400 bg-transparent font-semibold;
@apply btn px-2 border border-surface-400 bg-[#1e1e1e] font-semibold;
}

.btn-next {
Expand Down
51 changes: 51 additions & 0 deletions src/renderer/src/lib/helpers/launcherPersistedStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import localforage from 'localforage';

/**
* This is a Class acts as a unified accessor for all things stored in
* [localforage](https://www.npmjs.com/package/localforage).
*/
export class LauncherPersistedStore {
private store: LocalforageStore;

constructor(store?: LocalforageStore) {
this.store = store ? store : new LocalforageStore();
}

/**
* Stores the allowlist from the given URL in localStorage for it to be
* available also without internet access.
*/
allowList: SubStore<string, string, [string]> = {
value: async (url) => this.store.getItem<string>(`allowList${url}`),
set: async (value, url) => this.store.setItem<string>(`allowList${url}`, value)
};

/**
* Stores that the Quick Setup option was chosen in case that setup
* fails and the user restarts when lair keystore is already initialized.
* Launcher will directly continue with the Quick Setup steps after
* entering the password in this case.
*/
quickSetupChosen: SubStore<boolean, boolean, []> = {
value: () => this.store.getItem<boolean>('quickSetupChosen'),
set: (value) => this.store.setItem<boolean>('quickSetupChosen', value)
};
}

export interface SubStore<T, U, V extends any[]> {
value: (...args: V) => Promise<T | null>;
set: (value: U, ...args: V) => Promise<U>;
}

export class LocalforageStore {
getItem = async <T>(key: string): Promise<T | null> => localforage.getItem(key) as Promise<T>;

setItem = async <T>(key: string, value: T): Promise<T> => localforage.setItem(key, value);

removeItem = async (key: string) => localforage.removeItem(key);

clear = async () => localforage.clear();

keys = async () => localforage.keys();
}
11 changes: 6 additions & 5 deletions src/renderer/src/lib/helpers/other.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CellType, encodeHashToBase64, type CellInfo } from '@holochain/client';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CellType, encodeHashToBase64 } from '@holochain/client';
import type { AppstoreAppClient, AppVersionEntry, Entity } from 'appstore-tools';
import localforage from 'localforage';

Expand All @@ -18,7 +19,7 @@ import { AppstoreFilterListsSchema } from '$types/happs';
export function recursiveSum(obj: any): number {
let sum = 0;

for (let key in obj) {
for (const key in obj) {
if (typeof obj[key] === 'object') {
sum += recursiveSum(obj[key]);
} else {
Expand All @@ -31,8 +32,8 @@ export function recursiveSum(obj: any): number {

export function divideObject(obj: any, divider: number) {
const newObj: any = {};
for (let key in obj) {
newObj[key] = obj[key]/divider;
for (const key in obj) {
newObj[key] = obj[key] / divider;
}
return newObj;
}
Expand Down Expand Up @@ -105,7 +106,7 @@ export const base64ToArrayBuffer = (base64: string) => {

export const createImageUrl = (icon?: Uint8Array) => {
return icon ? URL.createObjectURL(new File([icon], 'icon')) : undefined;
}
};

export const initializeDefaultAppPorts = async (data: InitializeAppPorts) => {
const { appPort, appstoreAuthenticationToken, devhubAuthenticationToken } = data;
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/src/lib/locale/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"chooseAPassphrase": "Choose passphrase",
"choosePassword": "Choose a password to encrypt your data and private keys. You will always need this password to start Launcher.",
"city": "City",
"completeYourSetup": "Complete Your Setup",
"confirmPassphrase": "Confirm the passphrase",
"confirmPassword": "Confirm password",
"confirmYourPassphrase": "Confirm your passphrase",
Expand All @@ -38,6 +39,7 @@
"enabled": "Enabled",
"enterAppName": "Enter app name",
"enterNetworkSeed": "Enter network seed",
"enterPassword": "Enter Password",
"exportLogs": "Export Logs",
"factoryReset": "Factory Reset",
"factoryResetClick": "Click here to factory reset your Holochain Launcher",
Expand All @@ -46,6 +48,7 @@
"factoryResetHolochainLauncher": "Fully reset Holochain Launcher. This will delete all your apps alongside any associated data as well as your private keys.",
"factoryResetModalBody": "WARNING: This will delete all your apps alongside any data therein as well as your private keys.",
"fetchingAppData": "Fetching app data",
"finishAdvancedSetup": "Finish Advanced Setup",
"generatingKeyRecoveryFile": "Generating key recovery file...",
"getStarted": "Get started",
"gettingAppBytes": "Getting app bytes",
Expand Down Expand Up @@ -111,13 +114,15 @@
"region": "Region",
"releases": "Releases",
"reset": "Reset",
"restartedWithSetupIncompleteNotice": "It looks like launcher got restarted before the advanced setup process completed successfully.",
"roleName": "Role Name",
"searchForApps": "Search for apps",
"setUpYourPublisherProfile": "Set up your publisher profile",
"setLauncherPassword": "Set Launcher Password",
"settings": "Settings",
"setupError": "Setup Error",
"setupProgress": "Setup Progress",
"skipAndLaunch": "Skip and Launch (Quick Setup)",
"startingHolochain": "Starting Holochain...",
"startingLairKeystore": "Starting Lair Keystore...",
"show": "show",
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/lib/stores/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './hideUnverifiedApps';
export * from './importedKeys';
export * from './password';
export * from './setup';
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { KeyFile } from 'hc-launcher-rust-utils';
import { writable, type Writable } from 'svelte/store';
import { type Writable, writable } from 'svelte/store';

export const appPassword: Writable<string> = writable('');
export const recoveryKeysPassphrase: Writable<string> = writable('');
export const generatedKeyRecoveryFile: Writable<KeyFile | undefined> = writable(undefined);
export const setupProgress: Writable<string> = writable('');
Loading