Skip to content

Commit

Permalink
Merge branch 'main' into feat/ctx-refactor-splash-screen
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtPooki committed Jul 31, 2023
2 parents aec2293 + 2b22786 commit 7b8af6a
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 99 deletions.
20 changes: 20 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Security Policy

The IPFS protocol and its implementations are still in heavy development. This
means that there may be problems in our protocols, or there may be mistakes in
our implementations. We take security
vulnerabilities very seriously. If you discover a security issue, please immediately bring
it to our attention!

## Reporting a Vulnerability

If you find a vulnerability that may affect live deployments -- for example, by
exposing a remote execution exploit -- please **send your report privately** to
[email protected]. Please **DO NOT file a public issue**.

If the issue is a protocol weakness that cannot be immediately exploited or
something not yet deployed, discuss it openly.

## Reporting a non-security bug

For non-security bugs, please file a GitHub [issue](https://github.com/ipfs/ipfs-desktop/issues/new/choose).
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"scripts": {
"start": "cross-env NODE_ENV=development electron .",
"lint": "ts-standard && standard",
"lint:fix": "ts-standard --fix && standard --fix",
"test": "cross-env NODE_ENV=test playwright test 'test/unit/.*.spec.js'",
"test:e2e": "xvfb-maybe cross-env NODE_ENV=test playwright test -c test/e2e/playwright.config.js",
"postinstall": "run-s install-app-deps patch-deps",
Expand All @@ -17,7 +18,7 @@
"force-webui-download": "shx rm -rf assets/webui && run-s build:webui",
"build": "run-s clean build:webui",
"build:webui": "run-s build:webui:*",
"build:webui:download": "npx ipfs-or-gateway -c bafybeicyp7ssbnj3hdzehcibmapmpuc3atrsc4ch3q6acldfh4ojjdbcxe -p assets/webui/ -t 360000 --verbose -g \"https://dweb.link\" ",
"build:webui:download": "npx ipfs-or-gateway -c bafybeieqdeoqkf7xf4aozd524qncgiloh33qgr25lyzrkusbcre4c3fxay -p assets/webui/ -t 360000 --verbose -g \"https://dweb.link\" ",
"build:webui:minimize": "shx rm -rf assets/webui/static/js/*.map && shx rm -rf assets/webui/static/css/*.map",
"package": "shx rm -rf dist/ && run-s build && electron-builder --publish onTag"
},
Expand Down
18 changes: 17 additions & 1 deletion src/auto-updater/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function setup () {
// we download manually in 'update-available'
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.logger = logger

autoUpdater.on('error', err => {
logger.error(`[updater] ${err.toString()}`)
Expand Down Expand Up @@ -90,6 +91,21 @@ function setup () {
})
})

let progressPercentTimeout = null
autoUpdater.on('download-progress', ({ percent, bytesPerSecond }) => {
const logDownloadProgress = () => {
logger.info(`[updater] download progress is ${percent.toFixed(2)}% at ${bytesPerSecond} bps.`)
}
// log the percent, but not too often to avoid spamming the logs, but we should
// be sure we're logging at what percent any hiccup is occurring.
clearTimeout(progressPercentTimeout)
if (percent === 100) {
logDownloadProgress()
return
}
progressPercentTimeout = setTimeout(logDownloadProgress, 2000)
})

autoUpdater.on('update-downloaded', ({ version }) => {
logger.info(`[updater] update to ${version} downloaded`)

Expand Down Expand Up @@ -154,7 +170,7 @@ async function checkForUpdates () {
}

module.exports = async function () {
if (['test', 'development'].includes(process.env.NODE_ENV)) {
if (['test', 'development'].includes(process.env.NODE_ENV ?? '')) {
getCtx().setProp('manualCheckForUpdates', () => {
showDialog({
title: 'Not available in development',
Expand Down
8 changes: 8 additions & 0 deletions src/common/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ module.exports = Object.freeze({
logger.error(err)
},

warn: (msg, meta) => {
logger.warn(msg, meta)
},

debug: (msg) => {
logger.debug(msg)
},

logsPath,
addAnalyticsEvent
})
59 changes: 36 additions & 23 deletions src/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const pDefer = require('p-defer')
const logger = require('./common/logger')

/**
* @typedef {'tray-menu' | 'tray' | 'countlyDeviceId' | 'manualCheckForUpdates' | 'startIpfs' | 'stopIpfs' | 'restartIpfs' | 'getIpfsd' | 'launchWebUI' | 'webui' | 'splashScreen'} ContextProperties
* @typedef {'tray-menu' | 'tray' | 'tray-menu-state' | 'tray.update-menu' | 'countlyDeviceId' | 'manualCheckForUpdates' | 'startIpfs' | 'stopIpfs' | 'restartIpfs' | 'getIpfsd' | 'launchWebUI' | 'webui' | 'splashScreen'} ContextProperties
*/

/**
Expand All @@ -15,6 +15,16 @@ const logger = require('./common/logger')
* * Avoid circular dependencies and makes it easier to test modules in isolation.
* * Speed up startup time by only loading what we need when we need it.
*
*
* | Context property exists? | Is the backing promise fulfilled? | Method called | Is a deferred promise created? | Returned Value |
* |--------------------------|-----------------------------------|---------------|--------------------------------|----------------------------------------------------------------------------------------------------------|
* | No | N/A | GetProp | Yes | A newly created deferred promise(unfulfilled) |
* | No | N/A | SetProp | Yes | void |
* | Yes | No | GetProp | No | The found deferred promise (unfulfilled) |
* | Yes | No | SetProp | No | void |
* | Yes | Yes | GetProp | No | The found deferred promise (fulfilled) |
* | Yes | Yes | SetProp | No | We throw an error here. Any getProps called for the property prior to this would have a hanging promise. |
*
* @extends {Record<string, unknown>}
* @property {function} launchWebUI
*/
Expand Down Expand Up @@ -44,10 +54,14 @@ class Context {
* @returns {void}
*/
setProp (propertyName, value) {
if (this._properties.has(propertyName)) {
logger.error('[ctx] Property already exists')
throw new Error(`[ctx] Property ${String(propertyName)} already exists`)
}
logger.info(`[ctx] setting ${String(propertyName)}`)
try {
this._properties.set(propertyName, value)
this._resolvePropForValue(propertyName, value)
this._resolvePropToValue(propertyName, value)
} catch (e) {
logger.error(e)
}
Expand All @@ -62,17 +76,17 @@ class Context {
async getProp (propertyName) {
logger.info(`[ctx] getting ${String(propertyName)}`)

if (this._properties.has(propertyName)) {
const value = this._properties.get(propertyName)
if (value != null) {
logger.info(`[ctx] Found existing property ${String(propertyName)}`)
const value = this._properties.get(propertyName)
this._resolvePropForValue(propertyName, value)
this._resolvePropToValue(propertyName, value)
// @ts-ignore
return value
} else {
logger.info(`[ctx] Could not find property ${String(propertyName)}`)
}
// no value exists, create deferred promise and return the promise
return this._createDeferredForProp(propertyName)
return this._createDeferredForProp(propertyName).promise
}

/**
Expand All @@ -93,42 +107,41 @@ class Context {

/**
* Gets existing promise and resolves it with the given value.
* If no promise exists, it creates one and calls itself again. (this shouldn't be necessary but is a fallback for a gotcha)
*
* @private
* @template T
* @param {ContextProperties} propertyName
* @param {T} value
* @returns {void}
*/
_resolvePropForValue (propertyName, value) {
const deferred = this._promiseMap.get(propertyName)
if (deferred != null) {
logger.info(`[ctx] Resolving promise for ${String(propertyName)}`)
// we have a value and there is an unresolved deferred promise
deferred.resolve(value)
} else {
_resolvePropToValue (propertyName, value) {
let deferred = this._promiseMap.get(propertyName)
if (deferred == null) {
logger.info(`[ctx] No promise found for ${String(propertyName)}`)
this._createDeferredForProp(propertyName)
this._resolvePropForValue(propertyName, value)
deferred = this._createDeferredForProp(propertyName)
}
logger.info(`[ctx] Resolving promise for ${String(propertyName)}`)
deferred.resolve(value)
}

/**
* Returns the existing promise for a property if it exists.
* If not, one is created and set in the `_promiseMap`, then returned
*
* Returns the existing promise for a property if it exists. If not, one is created and set in the `_promiseMap`, then returned
* @private
* @template T
* @param {ContextProperties} propertyName
* @returns {Promise<T>}
* @returns {pDefer.DeferredPromise<T>}
*/
_createDeferredForProp (propertyName) {
const promiseVal = this._promiseMap.get(propertyName)
if (promiseVal == null) {
const deferred = pDefer()
let deferred = this._promiseMap.get(propertyName)
if (deferred == null) {
deferred = pDefer()
this._promiseMap.set(propertyName, deferred)
return deferred.promise
}

// @ts-expect-error - Need to fix generics
return promiseVal.promise
return deferred
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/dialogs/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ let hasErrored = false

const generateErrorIssueUrl = (e) => `https://github.com/ipfs-shipyard/ipfs-desktop/issues/new?labels=kind%2Fbug%2C+need%2Ftriage&template=bug_report.md&title=${encodeURI(issueTitle(e))}&body=${encodeURI(issueTemplate(e))}`.substring(0, 1999)

/**
* This will fail and throw another application error if electron hasn't booted up properly.
* @param {Error} e
* @returns
*/
function criticalErrorDialog (e) {
if (hasErrored) return
hasErrored = true
Expand Down
2 changes: 2 additions & 0 deletions src/handleError.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ const { criticalErrorDialog } = require('./dialogs')
*/
function handleError (err) {
if (err == null) {
logger.debug('[global handleError] No error to handle')
return
}
/**
* Ignore network errors that might happen during the execution.
*/
if ((/** @type Error */(err))?.stack?.includes('net::')) {
logger.debug('[global handleError] Ignoring network error')
return
}

Expand Down
130 changes: 65 additions & 65 deletions src/tray.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,71 +255,9 @@ function icon (status) {
// https://www.electronjs.org/docs/faq#my-apps-tray-disappeared-after-a-few-minutes
let tray = null

const updateMenu = async () => {
const ctx = getCtx()
const { status, gcRunning, isUpdating } = await ctx.getProp('tray-menu-state')
const errored = status === STATUS.STARTING_FAILED || status === STATUS.STOPPING_FAILED
const menu = await ctx.getProp('tray-menu')

menu.getMenuItemById('ipfsIsStarting').visible = status === STATUS.STARTING_STARTED && !gcRunning && !isUpdating
menu.getMenuItemById('ipfsIsRunning').visible = status === STATUS.STARTING_FINISHED && !gcRunning && !isUpdating
menu.getMenuItemById('ipfsIsStopping').visible = status === STATUS.STOPPING_STARTED && !gcRunning && !isUpdating
menu.getMenuItemById('ipfsIsNotRunning').visible = status === STATUS.STOPPING_FINISHED && !gcRunning && !isUpdating
menu.getMenuItemById('ipfsHasErrored').visible = errored && !gcRunning && !isUpdating
menu.getMenuItemById('runningWithGC').visible = gcRunning
menu.getMenuItemById('runningWhileCheckingForUpdate').visible = isUpdating

menu.getMenuItemById('startIpfs').visible = status === STATUS.STOPPING_FINISHED
menu.getMenuItemById('stopIpfs').visible = status === STATUS.STARTING_FINISHED
menu.getMenuItemById('restartIpfs').visible = (status === STATUS.STARTING_FINISHED || errored)

menu.getMenuItemById('webuiStatus').enabled = status === STATUS.STARTING_FINISHED
menu.getMenuItemById('webuiFiles').enabled = status === STATUS.STARTING_FINISHED
menu.getMenuItemById('webuiPeers').enabled = status === STATUS.STARTING_FINISHED
menu.getMenuItemById('webuiNodeSettings').enabled = status === STATUS.STARTING_FINISHED

menu.getMenuItemById('startIpfs').enabled = !gcRunning
menu.getMenuItemById('stopIpfs').enabled = !gcRunning
menu.getMenuItemById('restartIpfs').enabled = !gcRunning

menu.getMenuItemById(CONFIG_KEYS.AUTO_LAUNCH).enabled = supportsLaunchAtLogin()
menu.getMenuItemById('takeScreenshot').enabled = status === STATUS.STARTING_FINISHED

menu.getMenuItemById('moveRepositoryLocation').enabled = !gcRunning && status !== STATUS.STOPPING_STARTED
menu.getMenuItemById('runGarbageCollector').enabled = menu.getMenuItemById('ipfsIsRunning').visible && !gcRunning

menu.getMenuItemById('setCustomBinary').visible = !hasCustomBinary()
menu.getMenuItemById('clearCustomBinary').visible = hasCustomBinary()

menu.getMenuItemById('checkForUpdates').enabled = !isUpdating
menu.getMenuItemById('checkForUpdates').visible = !isUpdating
menu.getMenuItemById('checkingForUpdates').visible = isUpdating

if (status === STATUS.STARTING_FINISHED) {
tray.setImage(icon(on))
} else {
tray.setImage(icon(off))
}

// Update configuration checkboxes.
for (const key of Object.values(CONFIG_KEYS)) {
const enabled = store.get(key, false)
const item = menu.getMenuItemById(key)
if (item) {
// Not all items are present in all platforms.
item.checked = enabled
}
}

if (!IS_MAC && !IS_WIN) {
// On Linux, in order for changes made to individual MenuItems to take effect,
// you have to call setContextMenu again - https://electronjs.org/docs/api/tray
tray.setContextMenu(menu)
}
}

const setupMenu = async () => {
const ctx = getCtx()
const updateMenu = ctx.getFn('tray.update-menu')
const menu = await buildMenu()
ctx.setProp('tray-menu', menu)

Expand Down Expand Up @@ -365,9 +303,71 @@ module.exports = async function () {
tray.on('click', popupMenu)
}
tray.on('right-click', popupMenu)
tray.on('double-click', async () => {
launchWebUI('/')
tray.on('double-click', async () => launchWebUI('/'))

ctx.setProp('tray.update-menu', async () => {
const ctx = getCtx()
const { status, gcRunning, isUpdating } = await ctx.getProp('tray-menu-state')
const errored = status === STATUS.STARTING_FAILED || status === STATUS.STOPPING_FAILED
const menu = await ctx.getProp('tray-menu')

menu.getMenuItemById('ipfsIsStarting').visible = status === STATUS.STARTING_STARTED && !gcRunning && !isUpdating
menu.getMenuItemById('ipfsIsRunning').visible = status === STATUS.STARTING_FINISHED && !gcRunning && !isUpdating
menu.getMenuItemById('ipfsIsStopping').visible = status === STATUS.STOPPING_STARTED && !gcRunning && !isUpdating
menu.getMenuItemById('ipfsIsNotRunning').visible = status === STATUS.STOPPING_FINISHED && !gcRunning && !isUpdating
menu.getMenuItemById('ipfsHasErrored').visible = errored && !gcRunning && !isUpdating
menu.getMenuItemById('runningWithGC').visible = gcRunning
menu.getMenuItemById('runningWhileCheckingForUpdate').visible = isUpdating

menu.getMenuItemById('startIpfs').visible = status === STATUS.STOPPING_FINISHED
menu.getMenuItemById('stopIpfs').visible = status === STATUS.STARTING_FINISHED
menu.getMenuItemById('restartIpfs').visible = (status === STATUS.STARTING_FINISHED || errored)

menu.getMenuItemById('webuiStatus').enabled = status === STATUS.STARTING_FINISHED
menu.getMenuItemById('webuiFiles').enabled = status === STATUS.STARTING_FINISHED
menu.getMenuItemById('webuiPeers').enabled = status === STATUS.STARTING_FINISHED
menu.getMenuItemById('webuiNodeSettings').enabled = status === STATUS.STARTING_FINISHED

menu.getMenuItemById('startIpfs').enabled = !gcRunning
menu.getMenuItemById('stopIpfs').enabled = !gcRunning
menu.getMenuItemById('restartIpfs').enabled = !gcRunning

menu.getMenuItemById(CONFIG_KEYS.AUTO_LAUNCH).enabled = supportsLaunchAtLogin()
menu.getMenuItemById('takeScreenshot').enabled = status === STATUS.STARTING_FINISHED

menu.getMenuItemById('moveRepositoryLocation').enabled = !gcRunning && status !== STATUS.STOPPING_STARTED
menu.getMenuItemById('runGarbageCollector').enabled = menu.getMenuItemById('ipfsIsRunning').visible && !gcRunning

menu.getMenuItemById('setCustomBinary').visible = !hasCustomBinary()
menu.getMenuItemById('clearCustomBinary').visible = hasCustomBinary()

menu.getMenuItemById('checkForUpdates').enabled = !isUpdating
menu.getMenuItemById('checkForUpdates').visible = !isUpdating
menu.getMenuItemById('checkingForUpdates').visible = isUpdating

if (status === STATUS.STARTING_FINISHED) {
tray.setImage(icon(on))
} else {
tray.setImage(icon(off))
}

// Update configuration checkboxes.
for (const key of Object.values(CONFIG_KEYS)) {
const enabled = store.get(key, false)
const item = menu.getMenuItemById(key)
if (item) {
// Not all items are present in all platforms.
item.checked = enabled
}
}

if (!IS_MAC && !IS_WIN) {
// On Linux, in order for changes made to individual MenuItems to take effect,
// you have to call setContextMenu again - https://electronjs.org/docs/api/tray
tray.setContextMenu(menu)
}
})
const updateMenu = ctx.getFn('tray.update-menu')

ipcMain.on(ipcMainEvents.IPFSD, status => {
// @ts-ignore
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type CONFIG_KEYS = 'AUTO_LAUNCH' | 'AUTO_GARBAGE_COLLECTOR' | 'SCREENSHOT_SHORTCUT' | 'OPEN_WEBUI_LAUNCH' | 'MONOCHROME_TRAY_ICON' | 'EXPERIMENT_PUBSUB' | 'EXPERIMENT_PUBSUB_NAMESYS'
Loading

0 comments on commit 7b8af6a

Please sign in to comment.