From 80fd0c0ec30d991e6fe319308ecbfed1b64d480e Mon Sep 17 00:00:00 2001 From: jgresham Date: Thu, 13 Jun 2024 08:44:17 -0700 Subject: [PATCH 01/18] chore: bump podman 5.1.1 and min windows build --- src/main/docker/docker.ts | 1 - src/main/podman/install/install.ts | 2 +- src/main/podman/podman-desktop/windows-check.ts | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/docker/docker.ts b/src/main/docker/docker.ts index 55e75b720..86e5a1ac7 100644 --- a/src/main/docker/docker.ts +++ b/src/main/docker/docker.ts @@ -164,7 +164,6 @@ export const getContainerDetails = async (containerIds: string[]) => { ); let details; if (data?.object) { - prefer - destructuring; details = data?.object; } return details; diff --git a/src/main/podman/install/install.ts b/src/main/podman/install/install.ts index d7a55454b..e669fa748 100644 --- a/src/main/podman/install/install.ts +++ b/src/main/podman/install/install.ts @@ -5,7 +5,7 @@ import installOnLinux from './installOnLinux'; import installOnMac from './installOnMac'; import installOnWindows from './installOnWindows'; -export const PODMAN_LATEST_VERSION = '5.1.0'; +export const PODMAN_LATEST_VERSION = '5.1.1'; export const PODMAN_MIN_VERSION = '4.3.0'; const installPodman = async (): Promise => { diff --git a/src/main/podman/podman-desktop/windows-check.ts b/src/main/podman/podman-desktop/windows-check.ts index b8cee0371..2cc030c2d 100644 --- a/src/main/podman/podman-desktop/windows-check.ts +++ b/src/main/podman/podman-desktop/windows-check.ts @@ -1,5 +1,7 @@ /** * Based from WinCheck in podman-desktop repo src/extensions/podman/podman-install + * https://github.com/containers/podman-desktop/blob/main/extensions/podman/src/podman-install.ts#L555 + * and from microsoft docs at https://learn.microsoft.com/en-us/windows/wsl/install#prerequisites */ import * as os from 'node:os'; import logger from '../../logger'; @@ -11,12 +13,11 @@ export const getFailSystemRequirements = async (): Promise< > => { const failedRequirements: FailSystemRequirements[] = []; - const MIN_BUILD = 18362; + const MIN_BUILD = 19041; const winRelease = os.release(); let winBuild; if (winRelease.startsWith('10.0.')) { const splitRelease = winRelease.split('.'); - prefer - destructuring; winBuild = splitRelease[2]; } if (!winBuild || Number.parseInt(winBuild, 10) < MIN_BUILD) { From 6d99113bbcda857df0851ea405905c1bb1713a7d Mon Sep 17 00:00:00 2001 From: cornpotage <99524051+corn-potage@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:03:18 -0700 Subject: [PATCH 02/18] fix: user configured settings persistence on add node flow (#598) * temp config working for all options and node services * only reset temp config if user changes initial node package * added changes to splash flow --- .../AddNodeConfiguration.tsx | 68 ++++++++++++++++--- .../InitialClientConfigs.tsx | 6 +- .../AddNodeConfiguration/deepMerge.ts | 10 +++ .../AddNodeStepper/AddNodeStepper.tsx | 14 +++- .../AddNodeStepper/AddNodeStepperModal.tsx | 16 ++++- 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/renderer/Presentational/AddNodeConfiguration/AddNodeConfiguration.tsx b/src/renderer/Presentational/AddNodeConfiguration/AddNodeConfiguration.tsx index 214a154dd..650c75f46 100644 --- a/src/renderer/Presentational/AddNodeConfiguration/AddNodeConfiguration.tsx +++ b/src/renderer/Presentational/AddNodeConfiguration/AddNodeConfiguration.tsx @@ -53,6 +53,8 @@ export interface AddNodeConfigurationProps { nodeStorageLocation?: string; shouldHideTitle?: boolean; disableSaveButton?: (value: boolean) => void; + setTemporaryClientConfigValues?: (value: any) => void; + tempConfigValues?: ClientConfigValues; } const nodeSpecToSelectOption = (nodeSpec: NodeSpecification) => { @@ -75,6 +77,8 @@ const AddNodeConfiguration = ({ shouldHideTitle, onChange, disableSaveButton, + setTemporaryClientConfigValues, + tempConfigValues, }: AddNodeConfigurationProps) => { const { t } = useTranslation(); const [sNodePackageSpec, setNodePackageSpec] = @@ -100,6 +104,20 @@ const AddNodeConfiguration = ({ const [sNodePackageConfigValues, dispatchNodePackageConfigValues] = useReducer(mergeObjectReducer, {}); + const onChangeClientConfigValues = (value: any) => { + dispatchClientConfigValues(value); + setTemporaryClientConfigValues({ + payload: value, + }); + }; + + const onChangeNodePackageConfigValues = (value: any) => { + dispatchNodePackageConfigValues(value); + setTemporaryClientConfigValues({ + payload: value, + }); + }; + const [ sNodeStorageLocationFreeStorageGBs, setNodeStorageLocationFreeStorageGBs, @@ -229,11 +247,22 @@ const AddNodeConfiguration = ({ (service: NodePackageNodeServiceSpec) => { clients.push(service); - // Set the pre-selected client as the first for each service - const option = service.nodeOptions[0]; + const firstNodeOption = service.nodeOptions[0]; + const previousSelection = + tempConfigValues.clientSelections?.[service.serviceId]; + const nodeSpec = - typeof option === 'string' ? nodeLibrary?.[option] : option; - if (nodeSpec) { + typeof firstNodeOption === 'string' + ? nodeLibrary?.[firstNodeOption] + : firstNodeOption; + + if ( + previousSelection && + nodeSpec && + previousSelection.value !== nodeSpec.specId + ) { + clientSelections[service.serviceId] = previousSelection; + } else if (nodeSpec) { clientSelections[service.serviceId] = nodeSpecToSelectOption(nodeSpec); } @@ -263,7 +292,10 @@ const AddNodeConfiguration = ({ const defaultNodesStorageDetails = await electron.getNodesDefaultStorageLocation(); console.log('defaultNodesStorageDetails', defaultNodesStorageDetails); - setNodeStorageLocation(defaultNodesStorageDetails.folderPath); + setNodeStorageLocation( + tempConfigValues?.storageLocation || + defaultNodesStorageDetails.folderPath, + ); if (onChange) { onChange({ clientSelections: sClientSelections, @@ -288,7 +320,14 @@ const AddNodeConfiguration = ({ ) => { if (!newSelection) return; console.log('new selected client: ', newSelection); - setClientSelections({ ...sClientSelections, [serviceId]: newSelection }); + const updatedSelections = { + ...sClientSelections, + [serviceId]: newSelection, + }; + setClientSelections(updatedSelections); + setTemporaryClientConfigValues({ + payload: { clientSelections: updatedSelections }, + }); }; useEffect(() => { @@ -378,6 +417,11 @@ const AddNodeConfiguration = ({ storageLocation: storageLocationDetails.folderPath, }); } + setTemporaryClientConfigValues({ + payload: { + storageLocation: storageLocationDetails.folderPath, + }, + }); setNodeStorageLocationFreeStorageGBs( storageLocationDetails.freeStorageGBs, ); @@ -395,19 +439,21 @@ const AddNodeConfiguration = ({ {/* Initial node package settings, required */} {requiredNodePackageSpecs.length > 0 && ( )} {/* Initial client settings, required */} {requiredClientSpecs.length > 0 && ( )} {sIsAdvancedOptionsOpen && ( @@ -415,16 +461,18 @@ const AddNodeConfiguration = ({ {/* Initial node package settings, advanced */} {advancedNodePackageSpecs.length > 0 && ( )} {/* Initial client settings, advanced */} {advancedClientSpecs.length > 0 && ( )} diff --git a/src/renderer/Presentational/AddNodeConfiguration/InitialClientConfigs.tsx b/src/renderer/Presentational/AddNodeConfiguration/InitialClientConfigs.tsx index 1202d1e29..d3092b074 100644 --- a/src/renderer/Presentational/AddNodeConfiguration/InitialClientConfigs.tsx +++ b/src/renderer/Presentational/AddNodeConfiguration/InitialClientConfigs.tsx @@ -20,6 +20,7 @@ export type ClientConfigTranslations = { }; export interface InitialClientConfigsProps { + tempConfigValues: ConfigValuesMap; clientSpecs: NodeSpecification[]; required?: boolean; disableSaveButton?: (value: boolean) => void; @@ -27,6 +28,7 @@ export interface InitialClientConfigsProps { } const InitialClientConfigs = ({ + tempConfigValues, clientSpecs, required = false, disableSaveButton = () => {}, @@ -137,7 +139,9 @@ const InitialClientConfigs = ({ {Object.keys(sClientConfigTranslations).map((clientId: string) => { const clientConfigTranslation = sClientConfigTranslations[clientId]; const singleClientConfigValues = - (sClientConfigValues as ConfigValuesMap)[clientId] || {}; + tempConfigValues[clientId] || + (sClientConfigValues as ConfigValuesMap)[clientId] || + {}; return ( diff --git a/src/renderer/Presentational/AddNodeConfiguration/deepMerge.ts b/src/renderer/Presentational/AddNodeConfiguration/deepMerge.ts index 8b9367674..3128f59f4 100644 --- a/src/renderer/Presentational/AddNodeConfiguration/deepMerge.ts +++ b/src/renderer/Presentational/AddNodeConfiguration/deepMerge.ts @@ -192,4 +192,14 @@ export const mergeObjectReducer = (state: object, action: object) => { return merge(state, action); }; +export const mergeObjectReducerWithReset = ( + state: object, + action: { type?: string; payload?: { reset?: boolean } & object }, +) => { + if (action.payload?.reset) { + return {}; + } + return merge(state, action.payload || {}); +}; + export default merge; diff --git a/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx b/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx index ba5638d98..6f451fa2f 100644 --- a/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx +++ b/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx @@ -1,6 +1,6 @@ // This component could be made into a Generic "FullScreenStepper" component // Just make sure to always render each child so that children component state isn't cleard -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useReducer } from 'react'; import type { SystemRequirements } from '../../../common/systemRequirements'; import type { @@ -18,6 +18,7 @@ import NodeRequirements from '../NodeRequirements/NodeRequirements'; import PodmanInstallation from '../PodmanInstallation/PodmanInstallation'; import { componentContainer, container } from './addNodeStepper.css'; import { mergeSystemRequirements } from './mergeNodeRequirements'; +import { mergeObjectReducerWithReset } from '../AddNodeConfiguration/deepMerge.js'; import type { NodePackageSpecification } from '../../../common/nodeSpec'; import type { AddNodePackageNodeService } from '../../../main/nodePackageManager'; @@ -51,6 +52,11 @@ const AddNodeStepper = ({ onChange, modal = false }: AddNodeStepperProps) => { const [sNodeRequirements, setEthereumNodeRequirements] = useState(); const [sNodeStorageLocation, setNodeStorageLocation] = useState(); + const [tempConfigValues, setTemporaryClientConfigValues] = useReducer( + mergeObjectReducerWithReset, + {}, + ); + const [selectedNode, setSelectedNode] = useState(''); // Load ALL node spec's when AddNodeStepper is created // This can later be optimized to only retrieve NodeSpecs as needed @@ -85,6 +91,10 @@ const AddNodeStepper = ({ onChange, modal = false }: AddNodeStepperProps) => { nodeReqs = sNodeLibrary?.[ecValue]?.systemRequirements; } } + setSelectedNode(newValue); + if (selectedNode !== newValue) { + setTemporaryClientConfigValues({ payload: { reset: true } }); + } try { if (nodeReqs) { setEthereumNodeRequirements(nodeReqs); @@ -261,6 +271,8 @@ const AddNodeStepper = ({ onChange, modal = false }: AddNodeStepperProps) => { nodePackageLibrary={sNodePackageLibrary} nodeId={sNode?.node?.value} onChange={onChangeAddNodeConfiguration} + tempConfigValues={tempConfigValues} + setTemporaryClientConfigValues={setTemporaryClientConfigValues} /> ); stepImage = step1; diff --git a/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx b/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx index 728b81188..9f2fc97a5 100644 --- a/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx +++ b/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx @@ -1,6 +1,6 @@ // This component could be made into a Generic "FullScreenStepper" component // Just make sure to always render each child so that children component state isn't cleard -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useReducer } from 'react'; import type { SystemRequirements } from '../../../common/systemRequirements'; import type { @@ -17,6 +17,7 @@ import NodeRequirements from '../NodeRequirements/NodeRequirements'; import PodmanInstallation from '../PodmanInstallation/PodmanInstallation'; import { componentContainer, container } from './addNodeStepper.css'; import { mergeSystemRequirements } from './mergeNodeRequirements'; +import { mergeObjectReducerWithReset } from '../AddNodeConfiguration/deepMerge.js'; export interface AddNodeStepperModalProps { modal?: boolean; @@ -44,6 +45,11 @@ const AddNodeStepperModal = ({ useState(); const [sNodeRequirements, setNodeRequirements] = useState(); + const [tempConfigValues, setTemporaryClientConfigValues] = useReducer( + mergeObjectReducerWithReset, + {}, + ); + const [selectedNode, setSelectedNode] = useState(''); const onChangeAddNodeConfiguration = ( newValue: AddNodeConfigurationValues, @@ -108,10 +114,14 @@ const AddNodeStepperModal = ({ }); // clear step 1 (client selections) when user changes node (package) setEthereumNodeConfig(undefined); + setSelectedNode(nodeSelectOption.value); + if (selectedNode !== nodeSelectOption.value) { + setTemporaryClientConfigValues({ payload: { reset: true } }); + } console.log('AddNodeStepperModal setNode: config', config); setNodeConfig(config); }, - [], + [selectedNode], ); const onChangeDockerInstall = useCallback((newValue: string) => { @@ -141,6 +151,8 @@ const AddNodeStepperModal = ({ onChange={onChangeAddNodeConfiguration} disableSaveButton={disableSaveButton} shouldHideTitle + tempConfigValues={tempConfigValues} + setTemporaryClientConfigValues={setTemporaryClientConfigValues} /> ); break; From 60d48e83e7cda92d5e901d352f1ba2e636e472e6 Mon Sep 17 00:00:00 2001 From: cornpotage <99524051+corn-potage@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:01:28 -0700 Subject: [PATCH 03/18] feat: send error and event report (#602) * disable sentry based on preference * added note to restart * small style fix for node command --- assets/locales/en/translation.json | 4 +-- src/main/logger.ts | 6 +++-- src/main/main.ts | 25 +++++++++++++------ src/main/state/settings.ts | 4 +-- src/renderer/App.tsx | 20 +++++++++++---- .../NodeSettings/NodeSettings.css.ts | 3 +++ .../Preferences/Preferences.tsx | 4 +-- 7 files changed, 45 insertions(+), 21 deletions(-) diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index 6063020f4..9242f684b 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -72,8 +72,8 @@ "DetailsForYourNew": "Details for your new wallet network", "DesktopNotifications": "Desktop notifications", "Privacy": "Privacy", - "SendEventReports": "Send event reports", - "SendEventReportsDescription": "Enable to help measure app impact and client diversity shown at https://impact.nicenode.xyz", + "SendErrorEventReports": "Send error and event reports", + "SendErrorEventReportsDescription": "Enable to send error reports and events to help measure impact & client diversity shown at https://impact.nicenode.xyz/. Restart for changes to take effect.", "SendErrorReports": "Send error reports", "SendErrorReportsDescription": "Enabled by default in alpha releases to fix bugs and improve the app.", "PreReleaseUpdates": "Pre-release Updates", diff --git a/src/main/logger.ts b/src/main/logger.ts index b37a427fb..fa145d548 100644 --- a/src/main/logger.ts +++ b/src/main/logger.ts @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/electron/main'; import { app } from 'electron'; import { createLogger, format, transports } from 'winston'; import SentryTransport from './util/sentryWinstonTransport'; +import store from './state/store.js'; // import DailyRotateFile from 'winston-daily-rotate-file'; @@ -89,9 +90,10 @@ export const autoUpdateLogger = createLogger({ // // If we're not in production then log to the `console` with the format: // `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` +const errorEventReporting = store.get('settings.appIsEventReportingEnabled'); if ( - process.env.NODE_ENV === 'production' || - process.env.NODE_ENV === 'staging' + (errorEventReporting === null || errorEventReporting) && + (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'staging') ) { logger.add( new SentryTransport({ diff --git a/src/main/main.ts b/src/main/main.ts index e3842619c..e9f317eed 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -34,8 +34,11 @@ import * as processExit from './processExit'; import * as systemInfo from './systemInfo'; import * as tray from './tray'; import * as updater from './updater'; -import { resolveHtmlPath } from './util'; -import { fixPathEnvVar } from './util/fixPathEnvVar'; +import { + SETTINGS_KEY, + APP_IS_EVENT_REPORTING_ENABLED, +} from './state/settings.js'; +import store from './state/store.js'; if (process.env.NODE_ENV === 'development') { dotenv.config(); @@ -57,12 +60,18 @@ if (isTest && process.env.TEST_ENV === 'wdio') { // fixPathEnvVar(); logger.info(`NICENODE_ENV: ${process.env.NICENODE_ENV}`); logger.info(`MP_PROJECT_ENV: ${process.env.MP_PROJECT_ENV}`); -Sentry.init({ - dsn: process.env.SENTRY_DSN, - maxBreadcrumbs: 50, - debug: process.env.NODE_ENV === 'development', - environment: process.env.NICENODE_ENV || 'development', -}); + +const errorEventReporting = store.get( + `${SETTINGS_KEY}.${APP_IS_EVENT_REPORTING_ENABLED}`, +); +if (errorEventReporting === null || errorEventReporting) { + Sentry.init({ + dsn: process.env.SENTRY_DSN, + maxBreadcrumbs: 50, + debug: process.env.NODE_ENV === 'development', + environment: process.env.NICENODE_ENV || 'development', + }); +} let mainWindow: BrowserWindow | null = null; export const getMainWindow = () => mainWindow; diff --git a/src/main/state/settings.ts b/src/main/state/settings.ts index bba85a2c2..5ef42db60 100644 --- a/src/main/state/settings.ts +++ b/src/main/state/settings.ts @@ -9,7 +9,7 @@ import { setOpenAtLoginLinux } from '../util/linuxAutostartFile'; import store from './store'; // export type Settings = Record; -const SETTINGS_KEY = 'settings'; +export const SETTINGS_KEY = 'settings'; const OS_PLATFORM_KEY = 'osPlatform'; const OS_ARCHITECTURE = 'osArchitecture'; const OS_LANGUAGE_KEY = 'osLanguage'; @@ -21,7 +21,7 @@ const APP_HAS_SEEN_ALPHA_MODAL = 'appHasSeenAlphaModal'; const APP_THEME_SETTING = 'appThemeSetting'; const APP_IS_OPEN_ON_STARTUP = 'appIsOpenOnStartup'; const APP_IS_NOTIFICATIONS_ENABLED = 'appIsNotificationsEnabled'; -const APP_IS_EVENT_REPORTING_ENABLED = 'appIsEventReportingEnabled'; +export const APP_IS_EVENT_REPORTING_ENABLED = 'appIsEventReportingEnabled'; const APP_IS_PRE_RELEASE_UPDATES_ENABLED = 'appIsPreReleaseUpdatesEnabled'; export type ThemeSetting = 'light' | 'dark' | 'auto'; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 32f199bd2..98b675012 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -30,11 +30,21 @@ import { initialize as initializeIpcListeners } from './ipc'; import './reset.css'; import { useAppDispatch } from './state/hooks'; -Sentry.init({ - dsn: electron.SENTRY_DSN, - debug: true, -}); -reportEvent('OpenApp'); +async function initializeSentry() { + const userSettings = await electron.getSettings(); + if ( + userSettings.appIsEventReportingEnabled === null || + userSettings.appIsEventReportingEnabled + ) { + Sentry.init({ + dsn: electron.SENTRY_DSN, + debug: true, + }); + } + reportEvent('OpenApp'); +} + +initializeSentry(); const WindowContainer = ({ children }: { children: React.ReactNode }) => { return ( diff --git a/src/renderer/Presentational/NodeSettings/NodeSettings.css.ts b/src/renderer/Presentational/NodeSettings/NodeSettings.css.ts index 57f0348d5..415e9e831 100644 --- a/src/renderer/Presentational/NodeSettings/NodeSettings.css.ts +++ b/src/renderer/Presentational/NodeSettings/NodeSettings.css.ts @@ -16,6 +16,9 @@ export const nodeCommandContainer = style({ export const nodeCommand = style({ fontFamily: 'monospace', + fontSize: 11, + lineHeight: '14px', + color: vars.color.font50, }); export const emptyContainer = style({ diff --git a/src/renderer/Presentational/Preferences/Preferences.tsx b/src/renderer/Presentational/Preferences/Preferences.tsx index 846cc7b58..bce8212f1 100644 --- a/src/renderer/Presentational/Preferences/Preferences.tsx +++ b/src/renderer/Presentational/Preferences/Preferences.tsx @@ -225,8 +225,8 @@ const Preferences = ({ sectionTitle: '', items: [ { - label: t('SendEventReports'), - description: `${t('SendEventReportsDescription')}`, + label: t('SendErrorEventReports'), + description: `${t('SendErrorEventReportsDescription')}`, value: ( Date: Tue, 18 Jun 2024 11:37:28 -0700 Subject: [PATCH 04/18] chore: bump niro 3.0.2 and op-stack 1.7.7 --- src/common/NodeSpecs/nitro/nitro-v1.0.0.json | 2 +- src/common/NodeSpecs/op-geth/op-geth-v1.0.0.json | 2 +- src/common/NodeSpecs/op-node/op-node-v1.0.0.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/NodeSpecs/nitro/nitro-v1.0.0.json b/src/common/NodeSpecs/nitro/nitro-v1.0.0.json index 8ee89ae04..4bfa70017 100644 --- a/src/common/NodeSpecs/nitro/nitro-v1.0.0.json +++ b/src/common/NodeSpecs/nitro/nitro-v1.0.0.json @@ -6,7 +6,7 @@ "executionTypes": ["docker"], "defaultExecutionType": "docker", "imageName": "docker.io/offchainlabs/nitro-node", - "defaultImageTag": "v2.3.4-b4cc111", + "defaultImageTag": "v3.0.2-9efbc16", "input": { "defaultConfig": { "parentChainRpcUrl": "", diff --git a/src/common/NodeSpecs/op-geth/op-geth-v1.0.0.json b/src/common/NodeSpecs/op-geth/op-geth-v1.0.0.json index 1e665cf02..262262e83 100644 --- a/src/common/NodeSpecs/op-geth/op-geth-v1.0.0.json +++ b/src/common/NodeSpecs/op-geth/op-geth-v1.0.0.json @@ -25,7 +25,7 @@ } }, "imageName": "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth", - "defaultImageTag": "v1.101311.0" + "defaultImageTag": "v1.101315.2" }, "category": "L2/ExecutionClient", "rpcTranslation": "eth-l2", diff --git a/src/common/NodeSpecs/op-node/op-node-v1.0.0.json b/src/common/NodeSpecs/op-node/op-node-v1.0.0.json index 963dd63b7..9decaf79a 100644 --- a/src/common/NodeSpecs/op-node/op-node-v1.0.0.json +++ b/src/common/NodeSpecs/op-node/op-node-v1.0.0.json @@ -22,7 +22,7 @@ } }, "imageName": "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-node", - "defaultImageTag": "v1.7.5" + "defaultImageTag": "v1.7.7" }, "category": "L2/ConsensusClient", "rpcTranslation": "eth-l2-consensus", From 3a5944b5c0104cb61d8bcb3fe43d668f4ecb8f5a Mon Sep 17 00:00:00 2001 From: jgresham Date: Tue, 18 Jun 2024 15:09:11 -0700 Subject: [PATCH 05/18] chore: safe lint fix --- src/main/logger.ts | 2 +- src/main/main.ts | 8 ++++---- .../Presentational/AddNodeStepper/AddNodeStepper.tsx | 4 ++-- .../Presentational/AddNodeStepper/AddNodeStepperModal.tsx | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/logger.ts b/src/main/logger.ts index fa145d548..3a5e2cef4 100644 --- a/src/main/logger.ts +++ b/src/main/logger.ts @@ -2,8 +2,8 @@ import path from 'node:path'; import * as Sentry from '@sentry/electron/main'; import { app } from 'electron'; import { createLogger, format, transports } from 'winston'; -import SentryTransport from './util/sentryWinstonTransport'; import store from './state/store.js'; +import SentryTransport from './util/sentryWinstonTransport'; // import DailyRotateFile from 'winston-daily-rotate-file'; diff --git a/src/main/main.ts b/src/main/main.ts index e9f317eed..e01d529b4 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -31,14 +31,14 @@ import { import { isLinux } from './platform'; import * as power from './power'; import * as processExit from './processExit'; -import * as systemInfo from './systemInfo'; -import * as tray from './tray'; -import * as updater from './updater'; import { - SETTINGS_KEY, APP_IS_EVENT_REPORTING_ENABLED, + SETTINGS_KEY, } from './state/settings.js'; import store from './state/store.js'; +import * as systemInfo from './systemInfo'; +import * as tray from './tray'; +import * as updater from './updater'; if (process.env.NODE_ENV === 'development') { dotenv.config(); diff --git a/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx b/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx index 6f451fa2f..8f6195021 100644 --- a/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx +++ b/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx @@ -1,6 +1,6 @@ // This component could be made into a Generic "FullScreenStepper" component // Just make sure to always render each child so that children component state isn't cleard -import { useCallback, useEffect, useState, useReducer } from 'react'; +import { useCallback, useEffect, useReducer, useState } from 'react'; import type { SystemRequirements } from '../../../common/systemRequirements'; import type { @@ -14,11 +14,11 @@ import { reportEvent } from '../../events/reportEvent'; import { useAppDispatch } from '../../state/hooks'; import { updateSelectedNodePackageId } from '../../state/node'; import AddNode, { type AddNodeValues } from '../AddNode/AddNode'; +import { mergeObjectReducerWithReset } from '../AddNodeConfiguration/deepMerge.js'; import NodeRequirements from '../NodeRequirements/NodeRequirements'; import PodmanInstallation from '../PodmanInstallation/PodmanInstallation'; import { componentContainer, container } from './addNodeStepper.css'; import { mergeSystemRequirements } from './mergeNodeRequirements'; -import { mergeObjectReducerWithReset } from '../AddNodeConfiguration/deepMerge.js'; import type { NodePackageSpecification } from '../../../common/nodeSpec'; import type { AddNodePackageNodeService } from '../../../main/nodePackageManager'; diff --git a/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx b/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx index 9f2fc97a5..2863c8eea 100644 --- a/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx +++ b/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx @@ -1,6 +1,6 @@ // This component could be made into a Generic "FullScreenStepper" component // Just make sure to always render each child so that children component state isn't cleard -import { useCallback, useEffect, useState, useReducer } from 'react'; +import { useCallback, useEffect, useReducer, useState } from 'react'; import type { SystemRequirements } from '../../../common/systemRequirements'; import type { @@ -12,12 +12,12 @@ import AddNode, { type AddNodeValues } from '../AddNode/AddNode'; import AddNodeConfiguration, { type AddNodeConfigurationValues, } from '../AddNodeConfiguration/AddNodeConfiguration'; +import { mergeObjectReducerWithReset } from '../AddNodeConfiguration/deepMerge.js'; import type { ModalConfig } from '../ModalManager/modalUtils'; import NodeRequirements from '../NodeRequirements/NodeRequirements'; import PodmanInstallation from '../PodmanInstallation/PodmanInstallation'; import { componentContainer, container } from './addNodeStepper.css'; import { mergeSystemRequirements } from './mergeNodeRequirements'; -import { mergeObjectReducerWithReset } from '../AddNodeConfiguration/deepMerge.js'; export interface AddNodeStepperModalProps { modal?: boolean; From ed3c16fcab8d4e370059bd1e6724554cca66750f Mon Sep 17 00:00:00 2001 From: jgresham Date: Wed, 19 Jun 2024 08:52:40 -0700 Subject: [PATCH 06/18] fix: erigon storage requirements --- src/common/NodeSpecs/erigon/erigon-v1.0.0.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/NodeSpecs/erigon/erigon-v1.0.0.json b/src/common/NodeSpecs/erigon/erigon-v1.0.0.json index b1966a232..8890cd602 100644 --- a/src/common/NodeSpecs/erigon/erigon-v1.0.0.json +++ b/src/common/NodeSpecs/erigon/erigon-v1.0.0.json @@ -273,12 +273,12 @@ { "value": "full", "config": "--prune=hrtc", - "info": "~800GB / ~2d" + "info": "~2TB / ~2d" }, { "value": "archive", "config": "", - "info": "~16TB" + "info": "~3.5TB" } ] }, From 94ccc2d5512c16ba718c1e12a2fbdf0a92c70c04 Mon Sep 17 00:00:00 2001 From: jgresham Date: Wed, 19 Jun 2024 09:12:09 -0700 Subject: [PATCH 07/18] chore: bump 6.1.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce98e8dd2..9d6c2ac38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nice-node", - "version": "6.1.3-alpha", + "version": "6.1.4-alpha", "description": "Run a node at home, the easy way.", "homepage": "https://nicenode.xyz", "productName": "NiceNode", From 78b12a879540ce9a2c915b86c2c4db43ed68b829 Mon Sep 17 00:00:00 2001 From: Johns Gresham Date: Thu, 20 Jun 2024 16:38:51 -0700 Subject: [PATCH 08/18] feat: load node library from api fallback to files (#611) * feat: load node library from api fallback to files * chore: remove debug step not used on gha e2e --- .github/workflows/e2e-test-linux-distros.yml | 6 - .github/workflows/e2e-test-mac.yml | 6 +- .github/workflows/e2e-test-ubuntu.yml | 5 - .github/workflows/e2e-test-windows.yml | 5 - .../NodeSpecs/arbitrum/arbitrum-v1.0.0.json | 2 +- src/common/NodeSpecs/nitro/nitro-v1.0.0.json | 2 +- src/main/cronJobs.ts | 9 ++ src/main/httpReq.ts | 6 + src/main/menu.ts | 13 ++ src/main/nn-auto-updater/githubReleases.ts | 2 - src/main/nodeLibraryManager.ts | 126 +++++++++++++----- 11 files changed, 125 insertions(+), 57 deletions(-) diff --git a/.github/workflows/e2e-test-linux-distros.yml b/.github/workflows/e2e-test-linux-distros.yml index 44fe5515e..7b22a6da8 100644 --- a/.github/workflows/e2e-test-linux-distros.yml +++ b/.github/workflows/e2e-test-linux-distros.yml @@ -89,9 +89,3 @@ jobs: - name: ๐Ÿงช Run Tests run: | npm run wdio - - - name: ๐Ÿ› Debug Build - uses: stateful/vscode-server-action@v1 - if: failure() - with: - timeout: '120000' diff --git a/.github/workflows/e2e-test-mac.yml b/.github/workflows/e2e-test-mac.yml index 79996de63..07fc0e632 100644 --- a/.github/workflows/e2e-test-mac.yml +++ b/.github/workflows/e2e-test-mac.yml @@ -46,8 +46,4 @@ jobs: uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a with: run: npm run wdio - - name: ๐Ÿ› Debug Build - uses: stateful/vscode-server-action@v1 - if: failure() - with: - timeout: '120000' + diff --git a/.github/workflows/e2e-test-ubuntu.yml b/.github/workflows/e2e-test-ubuntu.yml index 9ac88fb36..fafd933d3 100644 --- a/.github/workflows/e2e-test-ubuntu.yml +++ b/.github/workflows/e2e-test-ubuntu.yml @@ -44,8 +44,3 @@ jobs: with: run: npm run wdio - - name: ๐Ÿ› Debug Build - uses: stateful/vscode-server-action@v1 - if: failure() - with: - timeout: '120000' diff --git a/.github/workflows/e2e-test-windows.yml b/.github/workflows/e2e-test-windows.yml index ce8e6b72e..931149fa3 100644 --- a/.github/workflows/e2e-test-windows.yml +++ b/.github/workflows/e2e-test-windows.yml @@ -46,8 +46,3 @@ jobs: with: run: npm run wdio - - name: ๐Ÿ› Debug Build - uses: stateful/vscode-server-action@v1 - if: failure() - with: - timeout: '120000' diff --git a/src/common/NodeSpecs/arbitrum/arbitrum-v1.0.0.json b/src/common/NodeSpecs/arbitrum/arbitrum-v1.0.0.json index 163909014..ba5b66dfe 100644 --- a/src/common/NodeSpecs/arbitrum/arbitrum-v1.0.0.json +++ b/src/common/NodeSpecs/arbitrum/arbitrum-v1.0.0.json @@ -18,7 +18,7 @@ "category": "Ethereum/L2", "rpcTranslation": "eth-l1", "systemRequirements": { - "documentationUrl": "https://geth.arbitrum.org/docs/interface/hardware", + "documentationUrl": "https://docs.arbitrum.io/run-arbitrum-node/run-full-node#minimum-hardware-configuration", "cpu": { "cores": 4 }, diff --git a/src/common/NodeSpecs/nitro/nitro-v1.0.0.json b/src/common/NodeSpecs/nitro/nitro-v1.0.0.json index 4bfa70017..b864c2924 100644 --- a/src/common/NodeSpecs/nitro/nitro-v1.0.0.json +++ b/src/common/NodeSpecs/nitro/nitro-v1.0.0.json @@ -30,7 +30,7 @@ "category": "Ethereum/L2", "rpcTranslation": "eth-l2", "systemRequirements": { - "documentationUrl": "https://docs.arbitrum.io/node-running/how-tos/running-a-full-node#minimum-hardware-configuration", + "documentationUrl": "https://docs.arbitrum.io/node-running/how-tos/running-an-orbit-node#prerequisites", "cpu": { "cores": 4 }, diff --git a/src/main/cronJobs.ts b/src/main/cronJobs.ts index 0819622aa..073d732d7 100644 --- a/src/main/cronJobs.ts +++ b/src/main/cronJobs.ts @@ -3,6 +3,7 @@ import type { NodeId, NodeStatus, UserNodePackages } from '../common/node'; import { reportEvent } from './events'; import logger from './logger'; import { setLastRunningTime } from './node/setLastRunningTime'; +import { updateLocalNodeAndPackageLibrary } from './nodeLibraryManager.js'; import { getUserNodePackagesWithNodes } from './state/nodePackages'; import store from './state/store'; @@ -129,6 +130,13 @@ const hourlyNodesNodesLastRunningTimeJob = new CronJob( }, ); +// Run once everyday at midnight in the user's local timezone +const dailyNodeUpdateCheck = new CronJob(CRON_ONCE_A_DAY, async () => { + logger.info('Running cron dailyNodeUpdateCheck...'); + await updateLocalNodeAndPackageLibrary(); + logger.info('End cron dailyNodeUpdateCheck.'); +}); + export const onExit = () => { // Stop cron jobs on app exit dailyReportJob.stop(); @@ -139,6 +147,7 @@ export const initialize = () => { // Start the cron jobs and then run some for a first time now dailyReportJob.start(); hourlyNodesNodesLastRunningTimeJob.start(); + dailyNodeUpdateCheck.start(); // Wait 30 seconds for front end to load // todo: send report events from backend setTimeout(() => { diff --git a/src/main/httpReq.ts b/src/main/httpReq.ts index 7c8aec813..e4e0d5010 100644 --- a/src/main/httpReq.ts +++ b/src/main/httpReq.ts @@ -3,6 +3,12 @@ import https from 'node:https'; import logger from './logger'; +/** + * + * @param url + * @param options isHttp should be true if the url is http, false if https + * @returns + */ export const httpGet = ( url: string, options?: { diff --git a/src/main/menu.ts b/src/main/menu.ts index 2f10b8514..28857bdb9 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -17,6 +17,7 @@ import { reportEvent } from './events'; import i18nMain from './i18nMain'; import logger from './logger'; import { getFailSystemRequirements } from './minSystemRequirement'; +import { updateLocalNodeAndPackageLibrary } from './nodeLibraryManager.js'; import { removeAllNodePackages } from './nodePackageManager'; import nuclearUninstall from './nuclearUninstall'; import uninstallPodman from './podman/uninstall/uninstall'; @@ -285,6 +286,12 @@ export default class MenuBuilder { checkForPodmanUpdate(); }, }, + { + label: t('Check for Node Updates'), + click() { + updateLocalNodeAndPackageLibrary(); + }, + }, ], }; @@ -449,6 +456,12 @@ export default class MenuBuilder { checkForPodmanUpdate(); }, }, + { + label: t('Check for Node Updates'), + click() { + updateLocalNodeAndPackageLibrary(); + }, + }, ], }, ]; diff --git a/src/main/nn-auto-updater/githubReleases.ts b/src/main/nn-auto-updater/githubReleases.ts index a8bac59f9..452c42ac5 100644 --- a/src/main/nn-auto-updater/githubReleases.ts +++ b/src/main/nn-auto-updater/githubReleases.ts @@ -1,7 +1,5 @@ import { spawnSync } from 'node:child_process'; -import { arch, platform } from 'node:process'; import { app } from 'electron'; -import { find } from 'highcharts'; import { downloadFile } from '../downloadFile'; import { getNNDirPath } from '../files'; import logger from '../logger'; diff --git a/src/main/nodeLibraryManager.ts b/src/main/nodeLibraryManager.ts index 6015a0b65..3df1a9620 100644 --- a/src/main/nodeLibraryManager.ts +++ b/src/main/nodeLibraryManager.ts @@ -39,6 +39,7 @@ import type { NodeSpecification, DockerExecution as PodmanExecution, } from '../common/nodeSpec'; +import { httpGetJson } from './httpReq.js'; import logger from './logger'; import { type NodeLibrary, @@ -48,32 +49,102 @@ import { } from './state/nodeLibrary'; export const initialize = async () => { + await updateLocalNodeAndPackageLibrary(); +}; + +// todo: use user defined url if available +const getCartridgePackages = async (): Promise => { + // const cartridgePackagesApiURL = 'http://localhost:3000/api/cartridgePackage' + // const isHttp = true; + const cartridgePackagesApiURL = + 'https://api.nicenode.xyz/api/cartridgePackage'; + const isHttp = false; + const cartridgePackages: NodeSpecification[] = ( + await httpGetJson(cartridgePackagesApiURL, isHttp) + ).data; + // simple validation (only for nicenode api, not user defined api) + const isEthereumPackageFound = cartridgePackages.find( + (spec) => spec.specId === 'ethereum', + ); + if (!isEthereumPackageFound) { + throw new Error('Ethereum package not found in the cartridge packages API'); + } + return cartridgePackages; +}; + +const getCartridges = async (): Promise => { + // const cartridgesApiURL = 'http://localhost:3000/api/cartridge' + // const isHttp = true; + const cartridgesApiURL = 'https://api.nicenode.xyz/api/cartridge'; + const isHttp = false; + const cartridges: NodeSpecification[] = ( + await httpGetJson(cartridgesApiURL, isHttp) + ).data; + // simple validation (only for nicenode api, not user defined api) + const isGethFound = cartridges.find((spec) => spec.specId === 'geth'); + if (!isGethFound) { + throw new Error('Geth cartridge not found in the cartridge packages API'); + } + return cartridges; +}; + +// Updates the local electron store with the latest node and package library (aka cartridges) +// Should be called this after user clicks add node, but before showing the previous values +export const updateLocalNodeAndPackageLibrary = async () => { // parse spec json for latest versions // update the store with the latest versions + // get specs from APIs, fallback to files + + let specs: NodeSpecification[] = []; + let packageSpecs: NodeSpecification[] = []; + try { + const promises = [getCartridgePackages(), getCartridges()]; + // fetch in parallel + const [cartridgePackages, cartridges] = await Promise.all(promises); + logger.info( + `cartridgePackages from HTTP API: ${JSON.stringify(cartridgePackages)}`, + ); + logger.info(`cartridges from HTTP API: ${JSON.stringify(cartridges)}`); + specs = cartridges; + packageSpecs = cartridgePackages; + } catch (e) { + logger.error(e); + logger.error( + 'Failed to fetch cartridges from API, falling back to local files', + ); + packageSpecs = [ + ethereumv1, + farcasterv1, + arbitrumv1, + optimismv1, + basev1, + minecraftv1, + homeAssistantv1, + ]; + specs = [ + besuv1, + nethermindv1, + erigonv1, + gethv1, + rethv1, + lodestarv1, + nimbusv1, + tekuv1, + lighthousev1, + prysmv1, + arbitrumv1, + nitrov1, + pathfinderv1, + opGethv1, + opNodev1, + hildrv1, + magiv1, + hubblev1, + itzgMinecraftv1, + homeAssistantServicev1, + ]; + } const nodeSpecBySpecId: NodeLibrary = {}; - const specs = [ - besuv1, - nethermindv1, - erigonv1, - gethv1, - rethv1, - lodestarv1, - nimbusv1, - tekuv1, - lighthousev1, - prysmv1, - arbitrumv1, - nitrov1, - pathfinderv1, - opGethv1, - opNodev1, - hildrv1, - magiv1, - hubblev1, - itzgMinecraftv1, - homeAssistantServicev1, - ]; - specs.forEach((spec) => { try { const nodeSpec: NodeSpecification = spec as NodeSpecification; @@ -119,15 +190,6 @@ export const initialize = async () => { updateNodeLibrary(nodeSpecBySpecId); const nodePackageSpecBySpecId: NodePackageLibrary = {}; - const packageSpecs = [ - ethereumv1, - farcasterv1, - arbitrumv1, - optimismv1, - basev1, - minecraftv1, - homeAssistantv1, - ]; packageSpecs.forEach((spec) => { try { nodePackageSpecBySpecId[spec.specId] = spec as NodePackageSpecification; From 5e070ebdc9132eb32c09ec01857a888de1f84609 Mon Sep 17 00:00:00 2001 From: omahs <73983677+omahs@users.noreply.github.com> Date: Mon, 24 Jun 2024 23:56:53 +0200 Subject: [PATCH 09/18] fix: typos (#614) --- src/common/index.md | 8 ++++---- src/renderer/state/rpcExecuteTranslation.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/index.md b/src/common/index.md index 1d60ff9e8..76dbb6bb2 100644 --- a/src/common/index.md +++ b/src/common/index.md @@ -6,7 +6,7 @@ All available [Node Specs on Github](https://github.com/NiceNode/nice-node/tree/ ## Node Package Spec example -The top level specificiation +The top level specification ```json { @@ -157,8 +157,8 @@ The top level specificiation } }, "iconUrl": "https://ethereum.png", - "addNodeDescription": "Running a full etherum node is a two part story. Choosing minority clients are important for the health of the network.", - "description": "An Ethereum node holds a copy of the Ethereum blockchain and verifies the validity of every block, keeps it up-to-date with new blocks and thelps others to download and update their own copies of the chain. In the case of Ethereum a node consists of two parts: the execution client and the consensus client. These two clients work together to verify Ethereum's state. The execution client listens to new transactions broadcasted in the network, executes them in EVM, and holds the latest state and database of all current Ethereum data. The consensus client runs the Proof-of-Stake consensus algorithm, which enables the network to achieve agreement based on validated data from the execution client. A non-validating node does not get financial rewards but there are many benefits of running a node for any Ethereum user to consider, including privacy, security, reduced reliance on third-party servers, censorship resistance and improved health and decentralization of the network.", + "addNodeDescription": "Running a full etherum node is a two part story. Choosing minority clients is important for the health of the network.", + "description": "An Ethereum node holds a copy of the Ethereum blockchain and verifies the validity of every block, keeps it up-to-date with new blocks and helps others to download and update their own copies of the chain. In the case of Ethereum a node consists of two parts: the execution client and the consensus client. These two clients work together to verify Ethereum's state. The execution client listens to new transactions broadcasted in the network, executes them in EVM, and holds the latest state and database of all current Ethereum data. The consensus client runs the Proof-of-Stake consensus algorithm, which enables the network to achieve agreement based on validated data from the execution client. A non-validating node does not get financial rewards but there are many benefits of running a node for any Ethereum user to consider, including privacy, security, reduced reliance on third-party servers, censorship resistance and improved health and decentralization of the network.", "resources": [ { "label": "Run your own node", @@ -176,7 +176,7 @@ The top level specificiation ## Node (Service) Spec example -The bottom level specificiation +The bottom level specification ::: info Ignore references to binary (not currently supported) diff --git a/src/renderer/state/rpcExecuteTranslation.ts b/src/renderer/state/rpcExecuteTranslation.ts index ccfe957a9..24d12a306 100644 --- a/src/renderer/state/rpcExecuteTranslation.ts +++ b/src/renderer/state/rpcExecuteTranslation.ts @@ -249,7 +249,7 @@ export const executeTranslation = async ( // const resp = await StarkNetClient.request('starknet_syncing', []); const resp = await callJsonRpc('starknet_syncing', []); // StarkNetClient.re - console.log('starkent syncing resp: ', resp); + console.log('starknet syncing resp: ', resp); // const resp = await callFetch(`${beaconBaseUrl}/eth/v1/node/syncing`); if (resp?.data?.is_syncing !== undefined) { let syncPercent; From 78787e34317e727511d97f155be1883cb1ae8ceb Mon Sep 17 00:00:00 2001 From: Johns Gresham Date: Wed, 26 Jun 2024 02:37:07 -0700 Subject: [PATCH 10/18] chore: remove reth beta tag (#610) --- src/common/NodeSpecs/reth/reth-v1.0.0.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/NodeSpecs/reth/reth-v1.0.0.json b/src/common/NodeSpecs/reth/reth-v1.0.0.json index b26acf985..eb3bc1d2b 100644 --- a/src/common/NodeSpecs/reth/reth-v1.0.0.json +++ b/src/common/NodeSpecs/reth/reth-v1.0.0.json @@ -27,7 +27,6 @@ }, "category": "L1/ExecutionClient", "minorityClient": true, - "nodeReleasePhase": "beta", "rpcTranslation": "eth-l1", "systemRequirements": { "documentationUrl": "https://paradigmxyz.github.io/reth/installation/installation.html#hardware-requirements", From 33b31d8a5f49cccf9f0f71fdd4905556a6bf2fd2 Mon Sep 17 00:00:00 2001 From: cornpotage <99524051+corn-potage@users.noreply.github.com> Date: Fri, 28 Jun 2024 14:55:22 -0700 Subject: [PATCH 11/18] feat: added prysm quic support (#615) --- .../lighthouse/lighthouse-v1.0.0.json | 4 +-- src/common/NodeSpecs/prysm/prysm-v1.0.0.json | 27 +++++++++++++++++++ src/main/podman/podman.ts | 11 ++++++-- src/main/ports.ts | 9 ++++++- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json b/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json index 2c973e8d4..a98219edc 100644 --- a/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json +++ b/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json @@ -13,7 +13,7 @@ "websockets": "Enabled", "executionEndpoint": "http://host.containers.internal", "network": "Mainnet", - "quicPort": "Enabled", + "quic": "Enabled", "quicPortUdp": "9001" }, "docker": { @@ -114,7 +114,7 @@ "infoDescription": "NiceNode requires http connections", "documentation": "https://lighthouse-book.sigmaprime.io/api-bn.html#starting-the-server" }, - "quicPort": { + "quic": { "displayName": "QUIC Connections", "uiControl": { "type": "select/single", diff --git a/src/common/NodeSpecs/prysm/prysm-v1.0.0.json b/src/common/NodeSpecs/prysm/prysm-v1.0.0.json index 0500804d3..a62b51d3a 100644 --- a/src/common/NodeSpecs/prysm/prysm-v1.0.0.json +++ b/src/common/NodeSpecs/prysm/prysm-v1.0.0.json @@ -94,6 +94,33 @@ "defaultValue": "13000", "documentation": "https://docs.prylabs.network/docs/prysm-usage/parameters" }, + "quic": { + "displayName": "QUIC Connections", + "uiControl": { + "type": "select/single", + "controlTranslations": [ + { + "value": "Enabled", + "config": "--enable-quic" + }, + { + "value": "Disabled" + } + ] + }, + "defaultValue": "Disabled", + "documentation": "https://docs.prylabs.network/docs/prysm-usage/parameters" + }, + "quicPort": { + "displayName": "QUIC port", + "cliConfigPrefix": ["p2p-quic-port="], + "uiControl": { + "type": "text" + }, + "infoDescription": "Example value: 13000", + "defaultValue": "13000", + "documentation": "https://docs.prylabs.network/docs/prysm-usage/parameters" + }, "p2pPortsUdp": { "displayName": "P2P discovery port (UDP)", "cliConfigPrefix": "--p2p-udp-port=", diff --git a/src/main/podman/podman.ts b/src/main/podman/podman.ts index 8290dbc24..9b4d4d3a8 100644 --- a/src/main/podman/podman.ts +++ b/src/main/podman/podman.ts @@ -412,6 +412,7 @@ const createPodmanPortInput = ( httpPort, webSocketsPort, quicPortUdp = undefined, + quicPort = undefined, gRpcPort, } = configTranslation; const { @@ -422,6 +423,7 @@ const createPodmanPortInput = ( httpPort: configHttpPort, webSocketsPort: configWsPort, quicPortUdp: configQuicPortUdp, + quicPort: configQuicPort, gRpcPort: configGRpcPort, } = configValuesMap || {}; const result = []; @@ -464,9 +466,14 @@ const createPodmanPortInput = ( } // Handle quic port if it exists (only lighthouse) - const quicPortValue = configQuicPortUdp || quicPortUdp?.defaultValue; + const quicPortUdpValue = configQuicPortUdp || quicPortUdp?.defaultValue; + if (quicPortUdpValue) { + result.push(`-p ${quicPortUdpValue}:${quicPortUdpValue}/udp`); + } + + const quicPortValue = configQuicPort || quicPort?.defaultValue; if (quicPortValue) { - result.push(`-p ${quicPortValue}:${quicPortValue}/udp`); + result.push(`-p ${quicPortValue}:${quicPortValue}`); } // Handle grpc port diff --git a/src/main/ports.ts b/src/main/ports.ts index 3562c4cdb..48ec20558 100644 --- a/src/main/ports.ts +++ b/src/main/ports.ts @@ -114,6 +114,7 @@ export const assignPortsToNode = (node: Node): Node => { 'p2pPortsTcp', 'webSocketsPort', 'quicPortUdp', + 'quicPort', ]; // Add other relevant port types const executionService = getNodePackageByServiceNodeId( @@ -188,7 +189,13 @@ export const didPortsChange = ( ): boolean => { if (!node || !node.spec || !node.spec.configTranslation) return false; - const baseKeys = ['httpPort', 'enginePort', 'webSocketsPort', 'quicPortUdp']; + const baseKeys = [ + 'httpPort', + 'enginePort', + 'webSocketsPort', + 'quicPortUdp', + 'quicPort', + ]; const p2pKeys = ['p2pPortsUdp', 'p2pPortsTcp', 'gRpcPort']; const hasBaseKeyChanged = baseKeys.some((key) => { From 59b7b200a980238c34ed75172549af492d5912a6 Mon Sep 17 00:00:00 2001 From: Johns Gresham Date: Wed, 10 Jul 2024 04:43:44 -0700 Subject: [PATCH 12/18] feat: node updates (must manually check for updates) (#613) * feat: check for node updates. only checking part completed * release notes url. update detailed changes modal start * checkin: rename check to getCheckForControllerUpdate. modal ready for spec diff message * check-in: add node spec diff tool and modal with changes * feat: appy node update with result notification * injectDefaultControllerConfig for controllers * fix client update notifications. client release urls * cleanup controller api and add env var * couple missing js endings on file imports * feat: developer mode for dev info in app --- .vscode/settings.json | 3 + README.md | 1 + assets/locales/en/genericComponents.json | 6 + assets/locales/en/notifications.json | 4 + package-lock.json | 4 +- src/common/NodeSpecs/besu/besu-v1.0.0.json | 6 +- .../NodeSpecs/erigon/erigon-v1.0.0.json | 4 + src/common/NodeSpecs/geth/geth-v1.0.0.json | 5 + .../lighthouse/lighthouse-v1.0.0.json | 3 +- .../NodeSpecs/lodestar/lodestar-v1.0.0.json | 3 +- .../nethermind/nethermind-v1.0.0.json | 3 +- .../NodeSpecs/nimbus/nimbus-v1.0.0.json | 3 +- src/common/NodeSpecs/prysm/prysm-v1.0.0.json | 3 +- src/common/NodeSpecs/reth/reth-v1.0.0.json | 3 +- src/common/NodeSpecs/teku/teku-v1.0.0.json | 3 +- .../injectDefaultControllerConfig.ts | 52 +++++ src/common/node-spec-tool/specDiff.ts | 118 ++++++++++++ .../updateActiveControllerConfig.ts | 92 +++++++++ src/common/node-spec-tool/util.ts | 177 ++++++++++++++++++ src/common/node.ts | 3 + src/common/nodeSpec.ts | 14 +- src/main/consts/notifications.ts | 18 +- src/main/ipc.ts | 15 ++ src/main/nodeLibraryManager.ts | 157 ++++++++++------ src/main/nodeManager.ts | 77 ++++++++ src/main/preload.ts | 7 + src/main/state/settings.ts | 18 ++ .../Generics/redesign/Header/Header.tsx | 61 ++++-- .../Generics/redesign/Modal/modal.css.ts | 7 + .../redesign/UpdateCallout/UpdateCallout.tsx | 53 ++++-- src/renderer/Generics/redesign/consts.ts | 6 + .../ContentMultipleClients.tsx | 1 + .../ContentSingleClient.tsx | 7 + .../ControllerUpdate/ControllerUpdate.tsx | 76 ++++++++ .../ControllerUpdate/controllerUpdate.css.ts | 45 +++++ .../Presentational/DevMode/DevMode.tsx | 25 +++ .../ModalManager/ControllerUpdateModal.tsx | 47 +++++ .../ModalManager/ModalManager.tsx | 3 + .../ModalManager/PreferencesModal.tsx | 4 + .../ModalManager/modalUtils.tsx | 1 + .../NodePackageScreen/NodePackageScreen.tsx | 1 + .../Presentational/NodeScreen/NodeScreen.tsx | 20 ++ .../Preferences/Preferences.tsx | 21 +++ .../Preferences/PreferencesWrapper.tsx | 12 ++ src/renderer/preload.d.ts | 5 + 45 files changed, 1089 insertions(+), 108 deletions(-) create mode 100644 src/common/node-spec-tool/injectDefaultControllerConfig.ts create mode 100644 src/common/node-spec-tool/specDiff.ts create mode 100644 src/common/node-spec-tool/updateActiveControllerConfig.ts create mode 100644 src/common/node-spec-tool/util.ts create mode 100644 src/renderer/Presentational/ControllerUpdate/ControllerUpdate.tsx create mode 100644 src/renderer/Presentational/ControllerUpdate/controllerUpdate.css.ts create mode 100644 src/renderer/Presentational/DevMode/DevMode.tsx create mode 100644 src/renderer/Presentational/ModalManager/ControllerUpdateModal.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 1cf975881..7cb46aea7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,8 @@ "test/**/__snapshots__": true, "package-lock.json": true, "*.{css,sass,scss}.d.ts": true + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" } } diff --git a/README.md b/README.md index c54685375..6e49bd541 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ SENTRY_DSN=fake-token MP_PROJECT_TOKEN=fake-token MP_PROJECT_ENV=dev NICENODE_ENV=development +CONTROLLER_API_URL=http://localhost:3000/api ``` `SENTRY_DSN` and `MP_PROJECT_TOKEN` should be fake unless testing. Contact Johns, @jgresham, if you want to test new error or event reporting code. diff --git a/assets/locales/en/genericComponents.json b/assets/locales/en/genericComponents.json index 053c63c88..eabb86b8a 100644 --- a/assets/locales/en/genericComponents.json +++ b/assets/locales/en/genericComponents.json @@ -1,5 +1,6 @@ { "AboutSecondsRemaining": "About {{seconds}} seconds remaining", + "CheckForUpdates": "Check for updates...", "Confirm": "Confirm", "FinishingUp": "Finishing up...", "Hide": "Hide", @@ -55,6 +56,7 @@ "PodmanIsNotRunning": "Podman is not running", "ClickToStartPodman": "Click to start Podman", "UpdateAvailable": "Update available", + "Update": "Update", "NewVersionAvailable": "New version ready to install", "PodmanInstalling": "Installing...", "PodmanLoading": "Loading...", @@ -96,10 +98,14 @@ "SetUp": "Set up", "SkipForNow": "Skip for now", "UpdateClient": "Update your client", + "UpdateNamedClient": "Update {{client}}", + "UpdateStopClient": "{{client}} will stop momentarily.", "UpdateClientDescription": "{{client}} has been downloaded and is ready to install.", "InstallUpdate": "Install Update", "Skip": "Skip", "ViewReleaseNotes": "View release notes", + "ViewNamedReleaseNotes": "View {{name}} release notes", + "ViewDetailedChanges": "View detailed changes...", "InitialSyncInProgress": "Initial sync in progress...", "NodeSettings": "Node Settings...", "RemoveNode": "Remove Node...", diff --git a/assets/locales/en/notifications.json b/assets/locales/en/notifications.json index 9bc0f2d57..94a280e15 100644 --- a/assets/locales/en/notifications.json +++ b/assets/locales/en/notifications.json @@ -1,4 +1,8 @@ { + "ClientSuccessfulyUpdatedTitle": "Client successfully updated", + "ClientSuccessfulyUpdatedDescription": "{{variable}} has been updated", + "ClientUpdateErrorTitle": "Client update error", + "ClientUpdateErrorDescription": "An error occurred while updating {{variable}}", "LowDiskSpaceTitle": "Low disk space", "LowDiskSpaceDescription": "Disk space is lower than 40GB", "InternetConnectionDownTitle": "Internet connection down", diff --git a/package-lock.json b/package-lock.json index 89b8c396a..1c2a962c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nice-node", - "version": "6.1.3-alpha", + "version": "6.1.4-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nice-node", - "version": "6.1.3-alpha", + "version": "6.1.4-alpha", "license": "MIT", "dependencies": { "@reduxjs/toolkit": "^2.2.3", diff --git a/src/common/NodeSpecs/besu/besu-v1.0.0.json b/src/common/NodeSpecs/besu/besu-v1.0.0.json index c69007aaf..37931d7c7 100644 --- a/src/common/NodeSpecs/besu/besu-v1.0.0.json +++ b/src/common/NodeSpecs/besu/besu-v1.0.0.json @@ -410,12 +410,14 @@ "type": "text" }, "infoDescription": "Set to lower number to use less bandwidth", - "documentation": "https://besu.hyperledger.org/public-networks/reference/cli/options#max-peers" + "documentation": "https://besu.hyperledger.org/public-networks/reference/cli/options#max-peers", + "releaseNotesUrl": "https://github.com/NethermindEth/nethermind/releases" } }, "documentation": { "default": "https://besu.hyperledger.org/en/stable/", - "docker": "https://besu.hyperledger.org/en/stable/HowTo/Get-Started/Installation-Options/Run-Docker-Image/" + "docker": "https://besu.hyperledger.org/en/stable/HowTo/Get-Started/Installation-Options/Run-Docker-Image/", + "releaseNotesUrl": "https://github.com/hyperledger/besu/releases" }, "iconUrl": "https://clientdiversity.org/assets/img/execution-clients/besu-text-logo.png", "resources": [ diff --git a/src/common/NodeSpecs/erigon/erigon-v1.0.0.json b/src/common/NodeSpecs/erigon/erigon-v1.0.0.json index 8890cd602..25c7643a4 100644 --- a/src/common/NodeSpecs/erigon/erigon-v1.0.0.json +++ b/src/common/NodeSpecs/erigon/erigon-v1.0.0.json @@ -290,6 +290,10 @@ }, "category": "L1/ExecutionClient", "rpcTranslation": "eth-l1", + "documentation": { + "default": "https://erigon.tech/", + "releaseNotesUrl": "https://github.com/ledgerwatch/erigon/releases" + }, "iconUrl": "https://clientdiversity.org/assets/img/execution-clients/erigon-text-logo.png", "resources": [ { diff --git a/src/common/NodeSpecs/geth/geth-v1.0.0.json b/src/common/NodeSpecs/geth/geth-v1.0.0.json index 887d4c4af..6f999555f 100644 --- a/src/common/NodeSpecs/geth/geth-v1.0.0.json +++ b/src/common/NodeSpecs/geth/geth-v1.0.0.json @@ -333,5 +333,10 @@ "defaultValue": "localhost,host.containers.internal" } }, + "documentation": { + "default": "https://geth.ethereum.org/", + "docker": "https://geth.ethereum.org/docs/getting-started/installing-geth#docker-container", + "releaseNotesUrl": "https://github.com/ethereum/go-ethereum/releases" + }, "iconUrl": "https://clientdiversity.org/assets/img/execution-clients/geth-logo.png" } diff --git a/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json b/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json index a98219edc..c3f653783 100644 --- a/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json +++ b/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json @@ -199,7 +199,8 @@ }, "documentation": { "default": "https://lighthouse-book.sigmaprime.io/intro.html", - "docker": "https://lighthouse-book.sigmaprime.io/docker.html" + "docker": "https://lighthouse-book.sigmaprime.io/docker.html", + "releaseNotesUrl": "https://github.com/sigp/lighthouse/releases" }, "iconUrl": "https://clientdiversity.org/assets/img/consensus-clients/lighthouse-logo.png", "resources": [ diff --git a/src/common/NodeSpecs/lodestar/lodestar-v1.0.0.json b/src/common/NodeSpecs/lodestar/lodestar-v1.0.0.json index ae21087b8..dc38153f2 100644 --- a/src/common/NodeSpecs/lodestar/lodestar-v1.0.0.json +++ b/src/common/NodeSpecs/lodestar/lodestar-v1.0.0.json @@ -158,7 +158,8 @@ }, "documentation": { "default": "https://chainsafe.github.io/lodestar/", - "docker": "https://chainsafe.github.io/lodestar/installation/#install-with-docker" + "docker": "https://chainsafe.github.io/lodestar/installation/#install-with-docker", + "releaseNotesUrl": "https://github.com/ChainSafe/lodestar/releases" }, "iconUrl": "https://clientdiversity.org/assets/img/consensus-clients/lodestar-logo-text.png", "resources": [ diff --git a/src/common/NodeSpecs/nethermind/nethermind-v1.0.0.json b/src/common/NodeSpecs/nethermind/nethermind-v1.0.0.json index 4158ab392..2615c6405 100644 --- a/src/common/NodeSpecs/nethermind/nethermind-v1.0.0.json +++ b/src/common/NodeSpecs/nethermind/nethermind-v1.0.0.json @@ -303,7 +303,8 @@ "type": "text" }, "infoDescription": "Set to lower number to use less bandwidth", - "documentation": "https://docs.nethermind.io/nethermind/ethereum-client/configuration/network" + "documentation": "https://docs.nethermind.io/nethermind/ethereum-client/configuration/network", + "releaseNotesUrl": "https://github.com/NethermindEth/nethermind/releases" } }, "documentation": { diff --git a/src/common/NodeSpecs/nimbus/nimbus-v1.0.0.json b/src/common/NodeSpecs/nimbus/nimbus-v1.0.0.json index 673fc8cc5..1d29666a0 100644 --- a/src/common/NodeSpecs/nimbus/nimbus-v1.0.0.json +++ b/src/common/NodeSpecs/nimbus/nimbus-v1.0.0.json @@ -165,7 +165,8 @@ }, "documentation": { "default": "https://nimbus.guide/", - "docker": "https://nimbus.guide/docker.html" + "docker": "https://nimbus.guide/docker.html", + "releaseNotesUrl": "https://github.com/status-im/nimbus-eth2/releases" }, "iconUrl": "https://clientdiversity.org/assets/img/consensus-clients/nimbus-logo-text.png", "resources": [ diff --git a/src/common/NodeSpecs/prysm/prysm-v1.0.0.json b/src/common/NodeSpecs/prysm/prysm-v1.0.0.json index a62b51d3a..7e7d01b4a 100644 --- a/src/common/NodeSpecs/prysm/prysm-v1.0.0.json +++ b/src/common/NodeSpecs/prysm/prysm-v1.0.0.json @@ -176,7 +176,8 @@ }, "documentation": { "default": "https://docs.prylabs.network/docs/getting-started", - "docker": "https://docs.prylabs.network/docs/install/install-with-docker/" + "docker": "https://docs.prylabs.network/docs/install/install-with-docker/", + "releaseNotesUrl": "https://github.com/prysmaticlabs/prysm/releases" }, "iconUrl": "https://clientdiversity.org/assets/img/consensus-clients/prysm-logo.png", "resources": [ diff --git a/src/common/NodeSpecs/reth/reth-v1.0.0.json b/src/common/NodeSpecs/reth/reth-v1.0.0.json index eb3bc1d2b..340a5e9e0 100644 --- a/src/common/NodeSpecs/reth/reth-v1.0.0.json +++ b/src/common/NodeSpecs/reth/reth-v1.0.0.json @@ -332,7 +332,8 @@ } }, "documentation": { - "default": "https://paradigmxyz.github.io/reth" + "default": "https://paradigmxyz.github.io/reth", + "releaseNotesUrl": "https://github.com/paradigmxyz/reth/releases" }, "resources": [ { diff --git a/src/common/NodeSpecs/teku/teku-v1.0.0.json b/src/common/NodeSpecs/teku/teku-v1.0.0.json index 033dae004..fd47e3120 100644 --- a/src/common/NodeSpecs/teku/teku-v1.0.0.json +++ b/src/common/NodeSpecs/teku/teku-v1.0.0.json @@ -163,7 +163,8 @@ }, "documentation": { "default": "https://docs.teku.consensys.net/en/stable/", - "docker": "https://docs.teku.consensys.net/en/stable/HowTo/Get-Started/Installation-Options/Run-Docker-Image/" + "docker": "https://docs.teku.consensys.net/en/stable/HowTo/Get-Started/Installation-Options/Run-Docker-Image/", + "releaseNotesUrl": "https://github.com/ConsenSys/teku/releases" }, "iconUrl": "https://clientdiversity.org/assets/img/consensus-clients/teku-logo.png", "resources": [ diff --git a/src/common/node-spec-tool/injectDefaultControllerConfig.ts b/src/common/node-spec-tool/injectDefaultControllerConfig.ts new file mode 100644 index 000000000..1993ffd12 --- /dev/null +++ b/src/common/node-spec-tool/injectDefaultControllerConfig.ts @@ -0,0 +1,52 @@ +import type { + NodeSpecification, + DockerExecution as PodmanExecution, +} from '../nodeSpec.js'; + +/** + * Injects default controller configuration (cliInput, serviceVersion, etc.) + * into the nodeSpec. It does NOT overwrite existing values. + * Always updated is the default serviceVersion value to spec.excution.defaultImageTag + * For example, if `cliInput` is defined, it does NOT get overwritten. + * @param nodeSpec + */ +export const injectDefaultControllerConfig = (nodeSpec: NodeSpecification) => { + // "inject" serviceVersion and dataDir (todo) here. Universal for all nodes. + const execution = nodeSpec.execution as PodmanExecution; + let defaultImageTag = 'latest'; + // if the defaultImageTag is set in the spec use that, otherwise 'latest' + if (execution.defaultImageTag !== undefined) { + defaultImageTag = execution.defaultImageTag; + } + + if (!nodeSpec.configTranslation) { + nodeSpec.configTranslation = {}; + } + + if (!nodeSpec.configTranslation.cliInput) { + nodeSpec.configTranslation.cliInput = { + displayName: `${nodeSpec.displayName} CLI input`, + uiControl: { + type: 'text', + }, + defaultValue: '', + addNodeFlow: 'advanced', + infoDescription: 'Additional CLI input', + }; + } + if (!nodeSpec.configTranslation.serviceVersion) { + nodeSpec.configTranslation.serviceVersion = { + displayName: `${nodeSpec.displayName} version`, + uiControl: { + type: 'text', + }, + defaultValue: defaultImageTag, + addNodeFlow: 'advanced', + infoDescription: + 'Possible values: latest, v1.0.0, or stable. Check service documenation.', + }; + } + + // always update the default serviceVersion value to the latest excution.defaultImageTag + nodeSpec.configTranslation.serviceVersion.defaultValue = defaultImageTag; +}; diff --git a/src/common/node-spec-tool/specDiff.ts b/src/common/node-spec-tool/specDiff.ts new file mode 100644 index 000000000..d1189cbc3 --- /dev/null +++ b/src/common/node-spec-tool/specDiff.ts @@ -0,0 +1,118 @@ +import type { SelectControl, SelectTranslation } from '../nodeConfig.js'; +import type { DockerExecution, NodeSpecification } from '../nodeSpec.js'; +import { assert, compareObjects } from './util.js'; + +export type UserSpecDiff = { + message: string; +}; + +// Only time a user default would be overridden is if the user default is not in the new spec as +// an option on a select config. In that case, the default should be used. So we should highlight any +// removal of select options. +// returns a list of changes +export const calcUserSpecDiff = ( + oldSpec: NodeSpecification, + newSpec: NodeSpecification, +): UserSpecDiff[] => { + assert(oldSpec.specId === newSpec.specId, 'specId mismatch'); + assert( + oldSpec.version < newSpec.version, + 'newSpec version is not greater than oldSpec version', + ); + + const diffs: UserSpecDiff[] = []; + diffs.push({ + message: `Controller version: ${oldSpec.version} -> ${newSpec.version}`, + }); + if (oldSpec.displayName !== newSpec.displayName) { + diffs.push({ + message: `Name: ${oldSpec.displayName} -> ${newSpec.displayName}`, + }); + } + + /////// [start] Execution + const oldSpecExecution = oldSpec.execution as DockerExecution; + const newSpecExecution = newSpec.execution as DockerExecution; + if (oldSpecExecution.imageName !== newSpecExecution.imageName) { + diffs.push({ + message: `Download URL: ${oldSpecExecution.imageName} -> ${newSpecExecution.imageName}`, + }); + } + if (oldSpecExecution.defaultImageTag !== newSpecExecution.defaultImageTag) { + diffs.push({ + message: `${newSpec.displayName} Version: ${oldSpecExecution.defaultImageTag} -> ${newSpecExecution.defaultImageTag}`, + }); + } + /////// [end] Execution + + /////// [start] System Requirements + const oldSysReq = oldSpec.systemRequirements; + const newSysReq = newSpec.systemRequirements; + if (!compareObjects(oldSysReq, newSysReq)) { + let oldSysReqString = ''; + let newSysReqString = ''; + if (!compareObjects(oldSysReq?.cpu, newSysReq?.cpu)) { + oldSysReqString += ` CPU: ${JSON.stringify(oldSysReq?.cpu)}`; + newSysReqString += ` CPU: ${JSON.stringify(newSysReq?.cpu)}`; + } + if (!compareObjects(oldSysReq?.memory, newSysReq?.memory)) { + oldSysReqString += ` Memory: ${JSON.stringify(oldSysReq?.memory)}`; + newSysReqString += ` Memory: ${JSON.stringify(newSysReq?.memory)}`; + } + if (!compareObjects(oldSysReq?.storage, newSysReq?.storage)) { + oldSysReqString += ` Storage: ${JSON.stringify(oldSysReq?.storage)}`; + newSysReqString += ` Storage: ${JSON.stringify(newSysReq?.storage)}`; + } + if (!compareObjects(oldSysReq?.internet, newSysReq?.internet)) { + oldSysReqString += ` Internet: ${JSON.stringify(oldSysReq?.internet)}`; + newSysReqString += ` Internet: ${JSON.stringify(newSysReq?.internet)}`; + } + + diffs.push({ + message: `System requirements: ${oldSysReqString} -> ${newSysReqString}`, + }); + } + /////// [end] System Requirements + + /////// [start] Config Tralsations + const oldTranslations = oldSpec.configTranslation ?? {}; + const newTranslations = newSpec.configTranslation ?? {}; + const oldTranslationKeys = Object.keys(oldTranslations); + const newTranslationKeys = Object.keys(newTranslations); + for (const key of oldTranslationKeys) { + if (!newTranslationKeys.includes(key)) { + diffs.push({ + message: `Removed setting: ${oldTranslations[key]?.displayName}`, + }); + } else { + if (oldTranslations[key]?.uiControl?.type.includes('select')) { + const oldSelectOptions = ( + oldTranslations[key].uiControl as SelectControl + )?.controlTranslations as SelectTranslation[]; + const newSelectOptions = ( + newTranslations[key].uiControl as SelectControl + )?.controlTranslations as SelectTranslation[]; + if (!compareObjects(oldSelectOptions, newSelectOptions)) { + diffs.push({ + message: `Changed setting options: ${JSON.stringify( + oldSelectOptions, + )} -> ${JSON.stringify(newSelectOptions)}`, + }); + } + } + + // else, they're the same + } + } + for (const key of newTranslationKeys) { + if (!oldTranslationKeys.includes(key)) { + diffs.push({ + message: `New setting: ${newTranslations[key]?.displayName}`, + }); + } + // else, they were compared when iterating over oldTranslationKeys + } + /////// [end] Config Tralsations + + return diffs; +}; diff --git a/src/common/node-spec-tool/updateActiveControllerConfig.ts b/src/common/node-spec-tool/updateActiveControllerConfig.ts new file mode 100644 index 000000000..bfc355f41 --- /dev/null +++ b/src/common/node-spec-tool/updateActiveControllerConfig.ts @@ -0,0 +1,92 @@ +import type { + ConfigValuesMap, + SelectControl, + SelectTranslation, +} from '../nodeConfig.js'; +import type { NodeSpecification } from '../nodeSpec.js'; + +// Todo: ConfigValuesMap in nodeConfig.ts needs updated with string[] type +// export type ConfigValuesMap = Record; + +/** + * // Iterate over current controller config + // If the key is not in the new spec controls, delete it from the config + // if the key is in the new spec, keep it OR + // if it is a select control, keep it ONLY if it is in the new spec's value options. + // if it is not in the options, delete the key from the config. + // If the key exists but the type changes, delete the key from the config. (should we do?) + + // Special cases: `serviceVersion` is updated to the spec's execution.defaultImageTag + + This function should be run AFTER injectDefaultControllerConfig() is called on the newSpec. + + // Todo: New required config? notify user after or before update? + * @param newSpec + * @param currentControllerConfig + * @returns + */ +export const calcNewControllerConfig = ( + newSpec: NodeSpecification, + currentControllerConfig: ConfigValuesMap, +): ConfigValuesMap => { + const newControllerConfig: ConfigValuesMap = {}; + const newTranslations = newSpec.configTranslation ?? {}; + const newTranslationKeys = Object.keys(newTranslations); + + for (const [key, value] of Object.entries(currentControllerConfig)) { + console.log(`Current Controller Config:: ${key}: ${value}`); + if (newTranslationKeys.includes(key)) { + if (newTranslations[key]?.uiControl?.type.includes('select')) { + const newSelectOptions = ( + newTranslations[key].uiControl as SelectControl + )?.controlTranslations as SelectTranslation[]; + if (newTranslations[key]?.uiControl?.type === 'select/multiple') { + // Only keep values that are in the new controller options + if (Array.isArray(value)) { + const valuesToKeep = []; + for (const val in value) { + const isFound = newSelectOptions.find((option) => { + // if the option.config does + return option.value === val; + }); + if (isFound) { + valuesToKeep.push(val); + } + } + if (valuesToKeep.length > 0) { + newControllerConfig[key] = valuesToKeep; + } + } else { + console.error( + `Value for multi select should be an array. key: ${key} value: ${value}`, + ); + } + } else { + // single select + // make sure current value is in the new options + if (typeof value === 'string') { + if ( + newSelectOptions.map((option) => option.value).includes(value) + ) { + newControllerConfig[key] = value; + } + } else { + console.error( + `Value for single select should be a string. key: ${key} value: ${value}`, + ); + } + } + } else { + // Non-select control, no validation required when updating + newControllerConfig[key] = value; + } + } else { + console.log( + `Current Controller Config key NOT found :: ${key}: ${value}`, + ); + } + } + //// end loop over currentControllerConfig's key, values + + return newControllerConfig; +}; diff --git a/src/common/node-spec-tool/util.ts b/src/common/node-spec-tool/util.ts new file mode 100644 index 000000000..7ffe1cfff --- /dev/null +++ b/src/common/node-spec-tool/util.ts @@ -0,0 +1,177 @@ +export function assert(condition: boolean, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +/** + * Simple json.stringify comparison of two objects + */ +export function compareObjects(obj1: any, obj2: any) { + return JSON.stringify(obj1) === JSON.stringify(obj2); +} + +export const gethv1 = { + specId: 'geth', + version: '1.0.0', + displayName: 'Geth', + execution: { + executionTypes: ['docker'], + defaultExecutionType: 'docker', + input: { + defaultConfig: { + http: 'Enabled', + httpCorsDomains: 'http://localhost', + webSockets: 'Disabled', + httpVirtualHosts: 'localhost,host.containers.internal', + authVirtualHosts: 'localhost,host.containers.internal', + httpAddress: '0.0.0.0', + webSocketAddress: '0.0.0.0', + syncMode: 'snap', + }, + docker: { + containerVolumePath: '/root/.ethereum', + raw: '', + forcedRawNodeInput: + '--authrpc.addr 0.0.0.0 --authrpc.jwtsecret /root/.ethereum/jwtsecret --ipcdisable', + }, + }, + imageName: 'docker.io/ethereum/client-go', + defaultImageTag: 'stable', + }, + category: 'L1/ExecutionClient', + rpcTranslation: 'eth-l1', + systemRequirements: { + documentationUrl: 'https://geth.ethereum.org/docs/interface/hardware', + cpu: { + cores: 4, + }, + memory: { + minSizeGBs: 16, + }, + storage: { + minSizeGBs: 1600, + ssdRequired: true, + }, + internet: { + minDownloadSpeedMbps: 25, + minUploadSpeedMbps: 10, + }, + docker: { + required: true, + }, + }, + configTranslation: { + dataDir: { + displayName: 'Data location', + cliConfigPrefix: '--datadir ', + defaultValue: '~/.ethereum', + uiControl: { + type: 'filePath', + }, + infoDescription: + 'Data directory for the databases and keystore (default: "~/.ethereum")', + }, + network: { + displayName: 'Network', + defaultValue: 'Mainnet', + hideFromUserAfterStart: true, + uiControl: { + type: 'select/single', + controlTranslations: [ + { + value: 'Mainnet', + config: '--mainnet', + }, + { + value: 'Holesky', + config: '--holesky', + }, + { + value: 'Sepolia', + config: '--sepolia', + }, + ], + }, + }, + webSocketsPort: { + displayName: 'WebSockets JSON-RPC port', + cliConfigPrefix: '--ws.port ', + defaultValue: '8546', + uiControl: { + type: 'text', + }, + infoDescription: + 'The port (TCP) on which WebSocket JSON-RPC listens. The default is 8546. You must expose ports appropriately.', + documentation: + 'https://geth.ethereum.org/docs/rpc/server#websocket-server', + }, + webSocketApis: { + displayName: 'Enabled WebSocket APIs', + cliConfigPrefix: '--ws.api ', + defaultValue: ['eth', 'net', 'web3'], + valuesJoinStr: ',', + uiControl: { + type: 'select/multiple', + controlTranslations: [ + { + value: 'eth', + config: 'eth', + }, + { + value: 'net', + config: 'net', + }, + { + value: 'web3', + config: 'web3', + }, + { + value: 'debug', + config: 'debug', + }, + + { + value: 'personal', + config: 'personal', + }, + { + value: 'admin', + config: 'admin', + }, + ], + }, + }, + syncMode: { + displayName: 'Execution Client Sync Mode', + category: 'Syncronization', + uiControl: { + type: 'select/single', + controlTranslations: [ + { + value: 'snap', + config: '--syncmode snap', + info: '', + }, + { + value: 'full', + config: '--syncmode full', + info: '~800GB / ~2d', + }, + { + value: 'archive', + config: '--syncmode full --gcmode archive', + info: '~16TB', + }, + ], + }, + addNodeFlow: 'required', + defaultValue: 'snap', + hideFromUserAfterStart: true, + documentation: + 'https://geth.ethereum.org/docs/faq#how-does-ethereum-syncing-work', + }, + }, + iconUrl: + 'https://clientdiversity.org/assets/img/execution-clients/geth-logo.png', +}; diff --git a/src/common/node.ts b/src/common/node.ts index a1315354c..f9a62e130 100644 --- a/src/common/node.ts +++ b/src/common/node.ts @@ -34,6 +34,7 @@ export enum NodeStoppedBy { user = 'user', shutdown = 'shutdown', podmanUpdate = 'podmanUpdate', + nodeUpdate = 'nodeUpdate', } export type NodeConfig = { @@ -77,6 +78,7 @@ type Node = { lastStartedTimestampMs?: number; lastStoppedTimestampMs?: number; stoppedBy?: NodeStoppedBy; + updateAvailable?: boolean; }; type NodeMap = Record; export type UserNodes = { @@ -267,4 +269,5 @@ export const getImageTag = (node: Node): string => { } return imageTag; }; + export default Node; diff --git a/src/common/nodeSpec.ts b/src/common/nodeSpec.ts index 7e71780bd..db6405350 100644 --- a/src/common/nodeSpec.ts +++ b/src/common/nodeSpec.ts @@ -105,7 +105,12 @@ export type NodeSpecification = { // (ex. peers, syncing, latest block num, etc.) iconUrl?: string; category?: string; - documentation?: { default?: string; docker?: string; binary?: string }; + documentation?: { + default?: string; + docker?: string; + binary?: string; + releaseNotesUrl?: string; + }; resources?: LabelValuesSectionItemsProps[]; }; @@ -141,7 +146,12 @@ export type NodePackageSpecification = { // (ex. peers, syncing, latest block num, etc.) iconUrl?: string; category?: string; - documentation?: { default?: string; docker?: string; binary?: string }; + documentation?: { + default?: string; + docker?: string; + binary?: string; + releaseNotesUrl?: string; + }; addNodeDescription?: string; description?: string; resources?: LabelValuesSectionItemsProps[]; diff --git a/src/main/consts/notifications.ts b/src/main/consts/notifications.ts index dddfd29bb..4378a607f 100644 --- a/src/main/consts/notifications.ts +++ b/src/main/consts/notifications.ts @@ -40,20 +40,12 @@ export const NOTIFICATIONS = Object.freeze({ }, COMPLETED: { CLIENT_UPDATED: { - title: 'Client successfuly updated', - description: 'consensus client', + title: 'ClientSuccessfulyUpdatedTitle', + description: 'ClientSuccessfulyUpdatedDescription', status: STATUS.COMPLETED, limit: 0, }, }, - DOWNLOAD: { - UPDATE_AVAILABLE: { - title: 'Client successfuly updated', - description: 'consensus client', - status: STATUS.DOWNLOAD, - limit: 0, - }, - }, WARNING: { LOW_DISK_SPACE: { title: 'LowDiskSpaceTitle', @@ -79,5 +71,11 @@ export const NOTIFICATIONS = Object.freeze({ status: STATUS.WARNING, limit: 0, }, + CLIENT_UPDATE_ERROR: { + title: 'ClientUpdateErrorTitle', + description: 'ClientUpdateErrorDescription', + status: STATUS.WARNING, + limit: 0, + }, }, }); diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f678586db..d4228912e 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -28,6 +28,7 @@ import { getMainProcessUsage, updateNodeLastSyncedBlock, } from './monitor'; +import { getCheckForControllerUpdate } from './nodeLibraryManager.js'; import { addNode, deleteNodeStorage, @@ -39,6 +40,7 @@ import { stopNode, stopSendingNodeLogs, } from './nodeManager'; +import { applyNodeUpdate } from './nodeManager.js'; import { type AddNodePackageNodeService, addNodePackage, @@ -75,6 +77,7 @@ import { setNativeThemeSetting, setThemeSetting, } from './state/settings'; +import { getSetIsDeveloperModeEnabled } from './state/settings.js'; import store from './state/store'; import { getSystemInfo } from './systemInfo'; @@ -200,6 +203,12 @@ export const initialize = () => { // Node library ipcMain.handle('getNodeLibrary', getNodeLibrary); ipcMain.handle('getNodePackageLibrary', getNodePackageLibrary); + ipcMain.handle('getCheckForControllerUpdate', (_event, nodeId: NodeId) => { + return getCheckForControllerUpdate(nodeId); + }); + ipcMain.handle('applyNodeUpdate', (_event, nodeId: NodeId) => { + return applyNodeUpdate(nodeId); + }); // Podman ipcMain.handle('getIsPodmanInstalled', isPodmanInstalled); @@ -248,6 +257,12 @@ export const initialize = () => { return getSetIsPreReleaseUpdatesEnabled(isPreReleaseUpdatesEnabled); }, ); + ipcMain.handle( + 'getSetIsDeveloperModeEnabled', + (_event, isDeveloperModeEnabled?: boolean) => { + return getSetIsDeveloperModeEnabled(isDeveloperModeEnabled); + }, + ); // Notifications ipcMain.handle('getNotifications', getNotifications); diff --git a/src/main/nodeLibraryManager.ts b/src/main/nodeLibraryManager.ts index 3df1a9620..ea7d721ba 100644 --- a/src/main/nodeLibraryManager.ts +++ b/src/main/nodeLibraryManager.ts @@ -34,6 +34,9 @@ import nitrov1 from '../common/NodeSpecs/nitro/nitro-v1.0.0.json'; import homeAssistantServicev1 from '../common/NodeSpecs/home-assistant-service/home-assistant-service-v1.0.0.json'; import itzgMinecraftv1 from '../common/NodeSpecs/itzg-minecraft/itzg-minecraft-v1.0.0.json'; +import { injectDefaultControllerConfig } from '../common/node-spec-tool/injectDefaultControllerConfig.js'; +import type { NodeId } from '../common/node.js'; +import type Node from '../common/node.js'; import type { NodePackageSpecification, NodeSpecification, @@ -47,48 +50,72 @@ import { updateNodeLibrary, updateNodePackageLibrary, } from './state/nodeLibrary'; +import { getNode, updateNode } from './state/nodes.js'; + +// let controllerApiURL = `http://localhost:3000/api`; +let controllerApiURL = 'https://api.nicenode.xyz/api'; +let isControllerApiHttp = false; +if (process.env.CONTROLLER_API_URL) { + controllerApiURL = process.env.CONTROLLER_API_URL; +} +if ( + process.env.CONTROLLER_API_URL_IS_HTTP || + controllerApiURL.includes('localhost') +) { + isControllerApiHttp = true; +} export const initialize = async () => { await updateLocalNodeAndPackageLibrary(); }; // todo: use user defined url if available -const getCartridgePackages = async (): Promise => { - // const cartridgePackagesApiURL = 'http://localhost:3000/api/cartridgePackage' - // const isHttp = true; - const cartridgePackagesApiURL = - 'https://api.nicenode.xyz/api/cartridgePackage'; - const isHttp = false; - const cartridgePackages: NodeSpecification[] = ( - await httpGetJson(cartridgePackagesApiURL, isHttp) +const getControllerPackages = async (): Promise => { + const controllerPackagesApiURL = `${controllerApiURL}/controllerPackage`; + const isHttp = isControllerApiHttp; + const controllerPackages: NodeSpecification[] = ( + await httpGetJson(controllerPackagesApiURL, isHttp) ).data; // simple validation (only for nicenode api, not user defined api) - const isEthereumPackageFound = cartridgePackages.find( + const isEthereumPackageFound = controllerPackages.find( (spec) => spec.specId === 'ethereum', ); if (!isEthereumPackageFound) { - throw new Error('Ethereum package not found in the cartridge packages API'); + throw new Error( + 'Ethereum package not found in the controller packages API', + ); } - return cartridgePackages; + return controllerPackages; }; -const getCartridges = async (): Promise => { - // const cartridgesApiURL = 'http://localhost:3000/api/cartridge' - // const isHttp = true; - const cartridgesApiURL = 'https://api.nicenode.xyz/api/cartridge'; - const isHttp = false; - const cartridges: NodeSpecification[] = ( - await httpGetJson(cartridgesApiURL, isHttp) +const getControllers = async (): Promise => { + const controllersApiURL = `${controllerApiURL}/controller`; + const isHttp = isControllerApiHttp; + const controllers: NodeSpecification[] = ( + await httpGetJson(controllersApiURL, isHttp) ).data; // simple validation (only for nicenode api, not user defined api) - const isGethFound = cartridges.find((spec) => spec.specId === 'geth'); + const isGethFound = controllers.find((spec) => spec.specId === 'geth'); if (!isGethFound) { - throw new Error('Geth cartridge not found in the cartridge packages API'); + throw new Error('Geth controller not found in the controller packages API'); + } + return controllers; +}; + +const getController = async ( + controllerId: string, +): Promise => { + const controllersApiURL = `${controllerApiURL}/controller/${controllerId}`; + const isHttp = isControllerApiHttp; + const response = await httpGetJson(controllersApiURL, isHttp); + if (response.error) { + throw Error(response.error); } - return cartridges; + const controller: NodeSpecification = response.data; + return controller; }; -// Updates the local electron store with the latest node and package library (aka cartridges) +// Updates the local electron store with the latest node and package library (aka controllers) // Should be called this after user clicks add node, but before showing the previous values export const updateLocalNodeAndPackageLibrary = async () => { // parse spec json for latest versions @@ -98,19 +125,19 @@ export const updateLocalNodeAndPackageLibrary = async () => { let specs: NodeSpecification[] = []; let packageSpecs: NodeSpecification[] = []; try { - const promises = [getCartridgePackages(), getCartridges()]; + const promises = [getControllerPackages(), getControllers()]; // fetch in parallel - const [cartridgePackages, cartridges] = await Promise.all(promises); + const [controllerPackages, controllers] = await Promise.all(promises); logger.info( - `cartridgePackages from HTTP API: ${JSON.stringify(cartridgePackages)}`, + `controllerPackages from HTTP API: ${JSON.stringify(controllerPackages)}`, ); - logger.info(`cartridges from HTTP API: ${JSON.stringify(cartridges)}`); - specs = cartridges; - packageSpecs = cartridgePackages; + logger.info(`controllers from HTTP API: ${JSON.stringify(controllers)}`); + specs = controllers; + packageSpecs = controllerPackages; } catch (e) { logger.error(e); logger.error( - 'Failed to fetch cartridges from API, falling back to local files', + 'Failed to fetch controllers from API, falling back to local files', ); packageSpecs = [ ethereumv1, @@ -152,34 +179,8 @@ export const updateLocalNodeAndPackageLibrary = async () => { nodeSpec.configTranslation = {}; } - // "inject" serviceVersion and dataDir (todo) here. Universal for all nodes. - const execution = nodeSpec.execution as PodmanExecution; - let defaultImageTag = 'latest'; - // if the defaultImageTag is set in the spec use that, otherwise 'latest' - if (execution.defaultImageTag !== undefined) { - defaultImageTag = execution.defaultImageTag; - } - - nodeSpec.configTranslation.cliInput = { - displayName: `${spec.displayName} CLI input`, - uiControl: { - type: 'text', - }, - defaultValue: '', - addNodeFlow: 'advanced', - infoDescription: 'Additional CLI input', - }; - - nodeSpec.configTranslation.serviceVersion = { - displayName: `${spec.displayName} version`, - uiControl: { - type: 'text', - }, - defaultValue: defaultImageTag, - addNodeFlow: 'advanced', - infoDescription: - 'Caution Advised! Example value: latest, v1.0.0, stable. Consult service documentation for available versions.', - }; + // "inject" cliInput, serviceVersion, etc here. Universal for all nodes. dataDir (todo?) + injectDefaultControllerConfig(nodeSpec); nodeSpecBySpecId[spec.specId] = nodeSpec; } catch (err) { @@ -201,3 +202,45 @@ export const updateLocalNodeAndPackageLibrary = async () => { return updateNodePackageLibrary(nodePackageSpecBySpecId); }; + +/** + * + * @param nodeId + * @returns latest controller if there is a new version, or undefined if + * there is no update + */ +export const getCheckForControllerUpdate = async ( + nodeId: NodeId, +): Promise => { + // get node + // using node.url, fetch the latest version + // compare to node.spec.version + // if newer, update node.updateAvailable = true + const node: Node = getNode(nodeId); + if (node) { + const latestController: NodeSpecification = await getController( + node.spec.specId, + ); + logger.info( + `getCheckForControllerUpdate: latestController: ${JSON.stringify( + latestController, + )}`, + ); + if (node.spec.version < latestController.version) { + logger.info( + `getCheckForControllerUpdate: Node ${node.spec.displayName} has an update available`, + ); + node.updateAvailable = true; + updateNode(node); + return latestController; + } + logger.info( + `getCheckForControllerUpdate: Node ${node.spec.displayName} does NOT have an update available`, + ); + } else { + logger.error(`getCheckForControllerUpdate: Node ${nodeId} not found`); + } + node.updateAvailable = false; + updateNode(node); + return undefined; // throw +}; diff --git a/src/main/nodeManager.ts b/src/main/nodeManager.ts index 1e5259c59..839d5d5b7 100644 --- a/src/main/nodeManager.ts +++ b/src/main/nodeManager.ts @@ -7,16 +7,20 @@ import { createNode, isDockerNode, } from '../common/node'; +import { injectDefaultControllerConfig } from '../common/node-spec-tool/injectDefaultControllerConfig.js'; +import { calcNewControllerConfig } from '../common/node-spec-tool/updateActiveControllerConfig.js'; import type { ConfigTranslationMap, ConfigValuesMap, } from '../common/nodeConfig'; import type { NodeSpecification } from '../common/nodeSpec'; +import { NOTIFICATIONS } from './consts/notifications.js'; import { deleteDisk, getNodesDirPath, makeNodeDir } from './files'; import logger from './logger'; import { setLastRunningTime } from './node/setLastRunningTime'; import { initialize as initNodeLibrary } from './nodeLibraryManager'; +import { getCheckForControllerUpdate } from './nodeLibraryManager.js'; import { createRunCommand, sendLogsToUI as dockerSendLogsToUI, @@ -32,6 +36,8 @@ import { checkNodePortsAndNotify } from './ports'; import { getNodeLibrary } from './state/nodeLibrary'; import * as nodeStore from './state/nodes'; import { getSetPortHasChanged } from './state/nodes'; +import { getNode } from './state/nodes.js'; +import { addNotification } from './state/notifications.js'; export const addNode = async ( nodeSpec: NodeSpecification, @@ -269,6 +275,77 @@ const compareSpecsAndUpdate = ( } }; +/** + * Node status must be stopped + * Calls calcNewControllerConfig to get the new config, then updates the node.spec to newSpec. + * Todo: restart the node if it was running before the update + * @param nodeId + * @param newSpec + */ +export const applyNodeUpdate = async (nodeId: NodeId): Promise => { + // todo: could put this check after stopping? + const newSpec = await getCheckForControllerUpdate(nodeId); + let node = getNode(nodeId); + if (newSpec === undefined) { + logger.error('Unable to update node. No newer controller found.'); + addNotification( + NOTIFICATIONS.WARNING.CLIENT_UPDATE_ERROR, + node.spec.displayName, + ); + return false; + } + const isRunningBeforeUpdate = node.status === NodeStatus.running; + if (node.status !== NodeStatus.stopped) { + await stopNode(nodeId, NodeStoppedBy.nodeUpdate); + node = getNode(nodeId); + } + if ( + node.status !== NodeStatus.stopped && + node.status !== NodeStatus.errorStopping + ) { + addNotification( + NOTIFICATIONS.WARNING.CLIENT_UPDATE_ERROR, + node.spec.displayName, + ); + throw new Error( + 'Unable to stop node before updating. Node is not stopped or is not error stopping.', + ); + } + + node = getNode(nodeId); + node.status = NodeStatus.updating; + nodeStore.updateNode(node); + + // This should always be run before calcNewControllerConfig + injectDefaultControllerConfig(newSpec); + + // Get the new config - removes unsupported config values, etc. + const newConfigValuesMap = calcNewControllerConfig( + newSpec, + node.config.configValuesMap, + ); + node.config.configValuesMap = newConfigValuesMap; + node.spec = newSpec; + node.updateAvailable = false; + if (!isRunningBeforeUpdate) { + node.status = NodeStatus.stopped; + } + nodeStore.updateNode(node); + + // Successful update notification + addNotification( + NOTIFICATIONS.COMPLETED.CLIENT_UPDATED, + node.spec.displayName, + ); + + if (isRunningBeforeUpdate) { + // todo: wait to see if successful before creating notification? + startNode(nodeId); + } + + return true; +}; + /** * Called on app launch. * Check's node processes and updates internal NiceNode records. diff --git a/src/main/preload.ts b/src/main/preload.ts index 206b17a57..0ded69b3f 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -122,6 +122,10 @@ contextBridge.exposeInMainWorld('electron', { // Node library getNodeLibrary: () => ipcRenderer.invoke('getNodeLibrary'), getNodePackageLibrary: () => ipcRenderer.invoke('getNodePackageLibrary'), + getCheckForControllerUpdate: (nodeId: NodeId) => + ipcRenderer.invoke('getCheckForControllerUpdate', nodeId), + applyNodeUpdate: (nodeId: NodeId) => + ipcRenderer.invoke('applyNodeUpdate', nodeId), // Podman getIsPodmanInstalled: () => ipcRenderer.invoke('getIsPodmanInstalled'), @@ -162,6 +166,9 @@ contextBridge.exposeInMainWorld('electron', { isPreReleaseUpdatesEnabled, ); }, + getSetIsDeveloperModeEnabled: (isDeveloperModeEnabled?: boolean) => { + ipcRenderer.invoke('getSetIsDeveloperModeEnabled', isDeveloperModeEnabled); + }, // Notifications getNotifications: () => ipcRenderer.invoke('getNotifications'), diff --git a/src/main/state/settings.ts b/src/main/state/settings.ts index 5ef42db60..d622a1010 100644 --- a/src/main/state/settings.ts +++ b/src/main/state/settings.ts @@ -23,6 +23,7 @@ const APP_IS_OPEN_ON_STARTUP = 'appIsOpenOnStartup'; const APP_IS_NOTIFICATIONS_ENABLED = 'appIsNotificationsEnabled'; export const APP_IS_EVENT_REPORTING_ENABLED = 'appIsEventReportingEnabled'; const APP_IS_PRE_RELEASE_UPDATES_ENABLED = 'appIsPreReleaseUpdatesEnabled'; +export const APP_IS_DEVELOPER_MODE_ENABLED = 'appIsDeveloperModeEnabled'; export type ThemeSetting = 'light' | 'dark' | 'auto'; export type Settings = { @@ -37,6 +38,7 @@ export type Settings = { [APP_IS_NOTIFICATIONS_ENABLED]?: boolean; [APP_IS_EVENT_REPORTING_ENABLED]?: boolean; [APP_IS_PRE_RELEASE_UPDATES_ENABLED]?: boolean; + [APP_IS_DEVELOPER_MODE_ENABLED]?: boolean; }; /** @@ -191,6 +193,22 @@ export const getSetIsPreReleaseUpdatesEnabled = ( return savedIsPreReleaseUpdatesEnabled; }; +export const getSetIsDeveloperModeEnabled = ( + isDeveloperModeEnabled?: boolean, +) => { + if (isDeveloperModeEnabled !== undefined) { + logger.info(`Setting isDeveloperModeEnabled to ${isDeveloperModeEnabled}`); + store.set( + `${SETTINGS_KEY}.${APP_IS_DEVELOPER_MODE_ENABLED}`, + isDeveloperModeEnabled, + ); + } + const savedIsDeveloperModeEnabled: boolean = store.get( + `${SETTINGS_KEY}.${APP_IS_DEVELOPER_MODE_ENABLED}`, + ); + return savedIsDeveloperModeEnabled; +}; + // listen to OS theme updates nativeTheme.on('updated', () => { console.log("nativeTheme.on('updated')"); diff --git a/src/renderer/Generics/redesign/Header/Header.tsx b/src/renderer/Generics/redesign/Header/Header.tsx index 51af02f42..0c06e8276 100644 --- a/src/renderer/Generics/redesign/Header/Header.tsx +++ b/src/renderer/Generics/redesign/Header/Header.tsx @@ -1,6 +1,9 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import type { NodeSpecification } from '../../../../common/nodeSpec.js'; +import { modalRoutes } from '../../../Presentational/ModalManager/modalUtils.js'; +import electron from '../../../electronGlobal.js'; import { useAppDispatch } from '../../../state/hooks'; import { setModalState } from '../../../state/modal'; import Button, { type ButtonProps } from '../Button/Button'; @@ -31,6 +34,7 @@ type HeaderProps = { */ export const Header = ({ nodeOverview, isPodmanRunning }: HeaderProps) => { const { + nodeId, name, displayName, title, @@ -39,6 +43,7 @@ export const Header = ({ nodeOverview, isPodmanRunning }: HeaderProps) => { status, version, onAction, + documentation, } = nodeOverview; const [isCalloutDisplayed, setIsCalloutDisplayed] = useState(false); @@ -112,7 +117,7 @@ export const Header = ({ nodeOverview, isPodmanRunning }: HeaderProps) => { }} >