Skip to content

Commit

Permalink
fix: loading state of start/stop daemon
Browse files Browse the repository at this point in the history
  • Loading branch information
Bilb committed Oct 27, 2022
1 parent 826069c commit a66130d
Show file tree
Hide file tree
Showing 17 changed files with 357 additions and 121 deletions.
28 changes: 21 additions & 7 deletions lokinetProcessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export const invoke = async (
};

export interface ILokinetProcessManager {
doStartLokinetProcess: () => Promise<string | null>;
doStopLokinetProcess: () => Promise<string | null>;
nodeStartLokinetProcess: () => Promise<string | null>;
nodeStopLokinetProcess: () => Promise<string | null>;
}

let lokinetProcessManager: ILokinetProcessManager;
Expand Down Expand Up @@ -101,15 +101,29 @@ export const doStartLokinetProcess = async (jobId: string): Promise<void> => {

const manager = await getLokinetProcessManager();

const startStopResult = await manager.doStartLokinetProcess();
let startResult = await manager.nodeStartLokinetProcess();
logLineToAppSide(`Lokinet process start result: "${startResult}"`);

if (startStopResult) {
if (
startResult &&
startResult.includes('The requested service has already been started')
) {
// try to stop it and restart it?
logLineToAppSide(`Trying to restart the daemon...`);
const stopResult = await manager.nodeStopLokinetProcess();
logLineToAppSide(`restart stop: ${stopResult}`);

startResult = await manager.nodeStartLokinetProcess();
logLineToAppSide(`Lokinet process restart result: "${startResult}"`);
}

if (startResult) {
sendGlobalErrorToAppSide('error-start-stop');
}
sendIpcReplyAndDeleteJob(jobId, null, '');
} catch (e: any) {
logLineToAppSide(`Lokinet process start failed with ${e.message}`);
console.info('doStartLokinetProcess failed with', e);
console.info('nodeStartLokinetProcess failed with', e);
sendGlobalErrorToAppSide('error-start-stop');
sendIpcReplyAndDeleteJob(jobId, null, result);
}
Expand All @@ -124,12 +138,12 @@ export const doStopLokinetProcess = async (jobId: string): Promise<void> => {
logLineToAppSide('About to stop Lokinet process');

const manager = await getLokinetProcessManager();
await manager.doStopLokinetProcess();
await manager.nodeStopLokinetProcess();
sendIpcReplyAndDeleteJob(jobId, null, '');
} catch (e: any) {
logLineToAppSide(`Lokinet process stop failed with ${e.message}`);
sendIpcReplyAndDeleteJob(jobId, e.message, '');

console.info('doStopLokinetProcess failed with', e);
console.info('nodeStopLokinetProcess failed with', e);
}
};
4 changes: 2 additions & 2 deletions lokinetProcessManagerLinux.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ILokinetProcessManager } from './lokinetProcessManager';

export class LokinetLinuxProcessManager implements ILokinetProcessManager {
async doStartLokinetProcess(): Promise<string | null> {
async nodeStartLokinetProcess(): Promise<string | null> {
throw new Error('Not systemd: not supported yet');
}

async doStopLokinetProcess(): Promise<string | null> {
async nodeStopLokinetProcess(): Promise<string | null> {
throw new Error('Not systemd: not supported yet');
}
}
4 changes: 2 additions & 2 deletions lokinetProcessManagerMacOS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ function getLokinetControlLocation() {
}

export class LokinetMacOSProcessManager implements ILokinetProcessManager {
doStartLokinetProcess(): Promise<string | null> {
nodeStartLokinetProcess(): Promise<string | null> {
return invoke(getLokinetControlLocation(), ['--start']);
}

doStopLokinetProcess(): Promise<string | null> {
nodeStopLokinetProcess(): Promise<string | null> {
return invoke(getLokinetControlLocation(), ['--stop']);
}
}
4 changes: 2 additions & 2 deletions lokinetProcessManagerSystemd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const isSystemD = async (): Promise<boolean> => {
const lokinetService = 'lokinet.service';

export class LokinetSystemDProcessManager implements ILokinetProcessManager {
async doStartLokinetProcess(): Promise<string | null> {
async nodeStartLokinetProcess(): Promise<string | null> {
const result = await invoke('systemctl', [
'--no-block',
'start',
Expand All @@ -42,7 +42,7 @@ export class LokinetSystemDProcessManager implements ILokinetProcessManager {
return result;
}

async doStopLokinetProcess(): Promise<string | null> {
async nodeStopLokinetProcess(): Promise<string | null> {
return invoke('systemctl', ['--no-block', 'stop', lokinetService]);
}
}
4 changes: 2 additions & 2 deletions lokinetProcessManagerWindows.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ILokinetProcessManager, invoke } from './lokinetProcessManager';

export class LokinetWindowsProcessManager implements ILokinetProcessManager {
doStartLokinetProcess(): Promise<string | null> {
nodeStartLokinetProcess(): Promise<string | null> {
return invoke('net', ['start', 'lokinet']);
}

doStopLokinetProcess(): Promise<string | null> {
nodeStopLokinetProcess(): Promise<string | null> {
return invoke('net', ['stop', 'lokinet']);
}
}
13 changes: 11 additions & 2 deletions lokinetRpcCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export const getSummaryStatus = async (reply_tag: string): Promise<void> => {
await invoke('llarp.get_status', reply_tag, {});
};

export const isDaemonRunning = async (reply_tag: string): Promise<void> => {
await invoke('llarp.get_status', reply_tag, {});
};

export const addExit = async (
reply_tag: string,
exitAddress: string,
Expand Down Expand Up @@ -158,8 +162,13 @@ export const initialLokinetRpcDealer = async (): Promise<void> => {
}

dealer = new _zmq.Dealer({
sendTimeout: 1000,
connectTimeout: 5000
reconnectInterval: 250,
reconnectMaxInterval: 0,
sendTimeout: 500,
// receiveTimeout: 500,
heartbeatInterval: 500,
heartbeatTimeToLive: 500,
heartbeatTimeout: 750
});
// just trigger the non blocking loop
void loopDealerReceiving();
Expand Down
25 changes: 7 additions & 18 deletions src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
selectInitialDaemonStartDone,
setGlobalError,
updateFromDaemonStatus,
markExitNodesFromDaemon,
markAsStoppedFromSummaryTimedOut
} from '../features/statusSlice';
import { Provider, useDispatch, useSelector } from 'react-redux';
Expand All @@ -33,7 +32,7 @@ import { StatusErrorType } from '../../sharedIpc';
import { getThemeFromSettings } from './config';

export function appendToAppLogsOutsideRedux(logline: string): void {
store.dispatch(appendToApplogs(logline));
store?.dispatch?.(appendToApplogs(logline));
}

export function setErrorOutsideRedux(errorStatus: StatusErrorType): void {
Expand Down Expand Up @@ -63,12 +62,14 @@ const useSummaryStatusPolling = () => {
const parsedStatus = parseSummaryStatus(statusAsString);
// Send the update to the redux store.
dispatch(updateFromDaemonStatus({ daemonStatus: parsedStatus }));

const hasExitNodeChange =
store.getState().status.exitNodeFromDaemon !==
parsedStatus.exitNodeFromDaemon;
const hasExitAuthChange =
store.getState().status.exitAuthCodeFromDaemon !==
parsedStatus.exitAuthCodeFromDaemon;

if (hasExitNodeChange) {
dispatch(
appendToApplogs(
Expand All @@ -87,22 +88,10 @@ const useSummaryStatusPolling = () => {
);
}

if (hasExitNodeChange || hasExitAuthChange) {
dispatch(
markExitNodesFromDaemon({
exitNodeFromDaemon: parsedStatus.exitNodeFromDaemon,
exitAuthCodeFromDaemon: parsedStatus.exitAuthCodeFromDaemon
})
);
// the daemon told us we have an exit set but our current state says we have an error on the status.
// make sure to remove that error from the UI
if (
hasExitNodeChange &&
parsedStatus.exitNodeFromDaemon &&
globalError === 'error-add-exit'
) {
dispatch(setGlobalError(undefined));
}
// the daemon told us we have an exit set but our current state says we have an error on the status.
// make sure to remove that error from the UI
if (parsedStatus.exitNodeFromDaemon && globalError === 'error-add-exit') {
dispatch(setGlobalError(undefined));
}
} catch (e) {
console.log('getSummaryStatus() failed');
Expand Down
9 changes: 6 additions & 3 deletions src/app/components/Exit/ExitPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
selectExitNodeFromUser,
selectHasExitNodeEnabled,
selectHasExitTurningOff,
selectHasExitTurningOn
selectHasExitTurningOn,
selectNetworkReady
} from '../../../features/statusSlice';
import { VpnMode } from '../VpnInfos';
import { ExitInput, ExitSelector } from './ExitSelect';
Expand All @@ -35,6 +36,7 @@ const ConnectDisconnectButton = () => {
const theme = useTheme();

const daemonOrExitIsLoading = useSelector(selectDaemonOrExitIsLoading);
const networkReady = useSelector(selectNetworkReady);
const daemonIsRunning = useSelector(selectDaemonRunning);
const exitIsOn = useSelector(selectHasExitNodeEnabled);

Expand All @@ -53,15 +55,16 @@ const ConnectDisconnectButton = () => {
? theme.dangerColor
: exitTurningOn || exitTurningOff
? theme.backgroundColor
: theme.textColor;
: theme.textColor;

const buttonBackgroundColor =
exitTurningOff || exitTurningOn ? theme.textColor : theme.backgroundColor;

const authCodeFromUser = useSelector(selectAuthCodeFromUser);
const exitNodeFromUser = useSelector(selectExitNodeFromUser);

const buttonDisabled = daemonOrExitIsLoading || !daemonIsRunning; // || globalError === 'error-start-stop';
const buttonDisabled =
daemonOrExitIsLoading || !daemonIsRunning || !networkReady;

function onClick() {
if (buttonDisabled) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/GeneralInfos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const formatUptimeItem = (

const formatUptime = (uptimeInMs: number) => {
if (!uptimeInMs || uptimeInMs <= 0) {
return `${uptimeInMs || 0} sec`;
return '';
}
const seconds = uptimeInMs / 1000;
const d = Math.floor(seconds / (3600 * 24));
Expand Down
20 changes: 18 additions & 2 deletions src/app/components/PowerButton/PowerButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import styled from 'styled-components';

Expand All @@ -7,12 +8,14 @@ import {
selectDaemonRunning,
selectGlobalError,
selectDaemonIsLoading,
selectHasExitNodeEnabled
selectHasExitNodeEnabled,
markDaemonIsTurningOn
} from '../../../features/statusSlice';
import { stopLokinetDaemon, startLokinetDaemon } from '../../../features/thunk';

import { selectedTheme } from '../../../features/uiStatusSlice';
import { checkIfDaemonRunning } from '../../../ipc/ipcRenderer';
import { appendToAppLogsOutsideRedux } from '../../app';

import { PowerButtonIcon } from './PowerButtonIcon';
import { PowerButtonContainerBorder } from './PowerButtonSpinner';
Expand Down Expand Up @@ -93,6 +96,7 @@ const usePowerButtonContainerShadowStyle = () => {
};

export const PowerButton = (): JSX.Element => {
const dispatch = useDispatch();
const [isHovered, setIsHovered] = useState(false);

const daemonOrExitIsLoading = useSelector(selectDaemonOrExitIsLoading);
Expand All @@ -102,6 +106,9 @@ export const PowerButton = (): JSX.Element => {
const { shadow, buttonContainerBackground } = usePowerButtonStyles();

const onPowerButtonClick = async () => {
appendToAppLogsOutsideRedux(
`onPowerButtonClick: daemonOrExitIsLoading:${daemonOrExitIsLoading}, daemonIsRunning:${daemonIsRunning}, daemonIsLoading:${daemonIsLoading}, `
);
if (daemonOrExitIsLoading) {
// we are waiting for a refresh from lokinet, drop the click event

Expand All @@ -117,9 +124,18 @@ export const PowerButton = (): JSX.Element => {
if (daemonIsLoading) {
return;
}
const isDaemonAlreadyRunning = await checkIfDaemonRunning();
// start the spinner while we make sure the daemon is not running.
dispatch(markDaemonIsTurningOn(true));

// no need to wait here, we just want a one shot Are you ON call.
const isDaemonAlreadyRunning = await checkIfDaemonRunning(
'onPowerButtonClick'
);

if (!isDaemonAlreadyRunning) {
await startLokinetDaemon();
} else {
dispatch(markDaemonIsTurningOn(false));
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/app/components/PowerButton/PowerButtonIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export function usePowerButtonColor(isHovered: boolean) {
const hasExitEnabled = useSelector(selectHasExitNodeEnabled);
const hasExitLoading = useSelector(selectHasExitNodeChangeLoading);

let buttonColor = isHovered ? theme.textColor : theme.textColorSubtle;

let buttonColor = isHovered ? theme.textColor : theme.textColorSubtle;
if (daemonRunning || (hasExitEnabled && !hasExitLoading)) {
buttonColor = isHovered ? theme.textColorSubtle : theme.textColor;
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/tabs/AppLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const AppLogs = (): JSX.Element => {
>
{hasLogLine ? (
appLogs.map((logLine) => {
const separator = logLine.indexOf(':');
const separator = logLine.indexOf('ms:') + 3;
const timestamp = logLine.substring(0, separator);
const content = logLine.substring(separator);
return (
Expand Down
22 changes: 22 additions & 0 deletions src/app/promiseUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export async function runForAtLeast<T>(
toRun: () => Promise<T>,
timer: number
): Promise<T> {
const atLeastPromise = sleepFor(timer);
return new Promise((resolve, reject) => {
Promise.allSettled([toRun(), atLeastPromise]).then((result) => {
if (result[0].status === 'fulfilled') {
resolve(result[0].value);
}
reject(result[0].status);
});
});
}

async function sleepFor(timeout: number) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('sleepFor');
}, timeout);
});
}
32 changes: 31 additions & 1 deletion src/features/appLogsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const removeFirstElementIfNeeded = (appLogs: Array<string>) => {
return appLogs;
};

let firstLogPrintedAt: number | undefined;

export const appLogsSlice = createSlice({
name: 'appLogs',
initialState: initialStatusState,
Expand All @@ -33,7 +35,35 @@ export const appLogsSlice = createSlice({
) {
return state;
}
state.appLogs.push(`${Date.now()}: ${action.payload}`);

if (firstLogPrintedAt === undefined) {
firstLogPrintedAt = Date.now();
}
const diffWithFirstLog = Date.now() - firstLogPrintedAt;
const diffDate = new Date(diffWithFirstLog);
let offsetToPrint = '';
if (diffWithFirstLog > 1000 * 60 * 60 * 24) {
offsetToPrint += `${Math.floor(
diffWithFirstLog / (1000 * 60 * 60 * 24)
)}d`;
}

if (diffWithFirstLog > 1000 * 60 * 60) {
offsetToPrint += `${offsetToPrint ? ':' : ''}${diffDate.getHours()}h`;
}

if (diffWithFirstLog > 1000 * 60) {
offsetToPrint += `${offsetToPrint ? ':' : ''}${diffDate.getMinutes()}m`;
}
if (diffWithFirstLog > 1000) {
offsetToPrint += `${offsetToPrint ? ':' : ''}${diffDate.getSeconds()}s`;
}

offsetToPrint += `${
offsetToPrint ? ':' : ''
}${diffDate.getMilliseconds()}ms`;

state.appLogs.push(`${offsetToPrint}: ${action.payload}`);

// Remove the first item is the size is too big
state.appLogs = removeFirstElementIfNeeded(state.appLogs);
Expand Down

0 comments on commit a66130d

Please sign in to comment.