Skip to content

Commit

Permalink
fix: cleanup boot on clean install
Browse files Browse the repository at this point in the history
The very first time extension started was broken, node got restarted
multiple times due to config updates happening in lib/options.js

This change simplifies boot process to the point no restarts are
triggered by config updates on the first run. Landing page is also fixed
to properly receive notification about new peers being available.

We switched to ipfs.io for DNS resolution until DNSLink support lands in
js-ipfs and we get better understanding how to operate
chrome.sockets.udp API

Tests for relevant code paths are updated and embedded js-ipfs with
chrome.sockets now listens on custom ports, removing the need of
changing configuration if someone is already running go-ipfs or js-ipfs
  • Loading branch information
lidel committed Apr 10, 2019
1 parent 5c0b495 commit 2b63636
Show file tree
Hide file tree
Showing 12 changed files with 133 additions and 104 deletions.
6 changes: 6 additions & 0 deletions add-on/src/background/background.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
'use strict'
/* eslint-env browser, webextensions */

// Enable some debug output from js-ipfs
// (borrowed from https://github.com/ipfs-shipyard/ipfs-companion/pull/557)
// to include everything (mplex, libp2p, mss): localStorage.debug = '*'
localStorage.debug = 'jsipfs*,ipfs*,-*:mfs*,-*:ipns*,-ipfs:preload*'

const browser = require('webextension-polyfill')
const createIpfsCompanion = require('../lib/ipfs-companion')
const { onInstalled } = require('../lib/on-installed')
Expand Down
32 changes: 14 additions & 18 deletions add-on/src/landing-pages/welcome/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,22 @@ function createWelcomePageStore (i18n, runtime) {
return function welcomePageStore (state, emitter) {
state.isIpfsOnline = null
state.peerCount = null

const port = runtime.connect({ name: 'browser-action-port' })

const onMessage = (message) => {
if (message.statusUpdate) {
const peerCount = message.statusUpdate.peerCount
const isIpfsOnline = peerCount > -1

if (isIpfsOnline !== state.isIpfsOnline || peerCount !== state.peerCount) {
state.isIpfsOnline = isIpfsOnline
state.peerCount = peerCount
emitter.emit('render')
}
}
}

port.onMessage.addListener(onMessage)

let port
emitter.on('DOMContentLoaded', async () => {
emitter.emit('render')
port = runtime.connect({ name: 'browser-action-port' })
port.onMessage.addListener(async (message) => {
console.log('port.onMessage', message)
if (message.statusUpdate) {
const peerCount = message.statusUpdate.peerCount
const isIpfsOnline = peerCount > -1
if (isIpfsOnline !== state.isIpfsOnline || peerCount !== state.peerCount) {
state.isIpfsOnline = isIpfsOnline
state.peerCount = peerCount
emitter.emit('render')
}
}
})
})
}
}
Expand Down
3 changes: 2 additions & 1 deletion add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ module.exports = function createDnslinkResolver (getState) {
readDnslinkFromTxtRecord (fqdn) {
const state = getState()
let apiProvider
if (state.ipfsNodeType !== 'embedded' && state.peerCount !== offlinePeerCount) {
// TODO: fix DNS resolver for ipfsNodeType='embedded:chromesockets', for now use ipfs.io
if (!state.ipfsNodeType.startsWith('embedded') && state.peerCount !== offlinePeerCount) {
apiProvider = state.apiURLString
} else {
// fallback to resolver at public gateway
Expand Down
55 changes: 33 additions & 22 deletions add-on/src/lib/ipfs-client/embedded-chromesockets.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ let nodeHttpApi = null
// let httpServer = null
// let hapiServer = null

// Enable some debug output from js-ipfs
// (borrowed from https://github.com/ipfs-shipyard/ipfs-companion/pull/557)
// to include everything (mplex, libp2p, mss): localStorage.debug = '*'
localStorage.debug = 'jsipfs*,ipfs*,-*:mfs*,-*:ipns*,-ipfs:preload*'

const log = debug('ipfs-companion:client:embedded')
log.error = debug('ipfs-companion:client:embedded:error')

Expand All @@ -43,9 +38,11 @@ exports.init = function init (opts) {
hapiServer = startRawHapiServer(9092)
}
*/
log('init: embedded js-ipfs+chrome.sockets')
log('init embedded:chromesockets')

const defaultOpts = JSON.parse(optionDefaults.ipfsNodeConfig)

// TODO: check if below is needed after js-ipfs is released with DHT disabled
defaultOpts.libp2p = {
config: {
dht: {
Expand All @@ -65,21 +62,21 @@ exports.init = function init (opts) {
reject(error)
})
node.once('ready', async () => {
node.on('start', async () => {
node.once('start', async () => {
// HttpApi is off in browser context and needs to be started separately
try {
const httpServers = new HttpApi(node, ipfsOpts)
nodeHttpApi = await httpServers.start()
await updateConfigWithHttpEndpoints(node)
await updateConfigWithHttpEndpoints(node, opts)
resolve(node)
} catch (err) {
reject(err)
}
})
node.on('error', error => {
log.error('something went terribly wrong in embedded js-ipfs!', error)
})
try {
node.on('error', error => {
log.error('something went terribly wrong in embedded js-ipfs!', error)
})
await node.start()
} catch (err) {
reject(err)
Expand All @@ -88,21 +85,35 @@ exports.init = function init (opts) {
})
}

const multiaddr2httpUrl = (ma) => maToUri(ma.includes('/http') ? ma : multiaddr(ma).encapsulate('/http'))

// Update internal configuration to HTTP Endpoints from js-ipfs instance
async function updateConfigWithHttpEndpoints (ipfs) {
const ma = await ipfs.config.get('Addresses.Gateway')
log(`synchronizing Addresses.Gateway=${ma} to customGatewayUrl and ipfsNodeConfig`)
const httpGateway = maToUri(ma.includes('/http') ? ma : multiaddr(ma).encapsulate('/http'))
const ipfsNodeConfig = JSON.parse((await browser.storage.local.get('ipfsNodeConfig')).ipfsNodeConfig)
ipfsNodeConfig.config.Addresses.Gateway = ma
await browser.storage.local.set({
customGatewayUrl: httpGateway,
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
})
async function updateConfigWithHttpEndpoints (ipfs, opts) {
const localConfig = await browser.storage.local.get('ipfsNodeConfig')
if (localConfig && localConfig.ipfsNodeConfig) {
const gwMa = await ipfs.config.get('Addresses.Gateway')
const apiMa = await ipfs.config.get('Addresses.API')
const httpGateway = multiaddr2httpUrl(gwMa)
const httpApi = multiaddr2httpUrl(apiMa)
log(`updating extension configuration to Gateway=${httpGateway} and API=${httpApi}`)
// update ports in JSON configuration for embedded js-ipfs
const ipfsNodeConfig = JSON.parse(localConfig.ipfsNodeConfig)
ipfsNodeConfig.config.Addresses.Gateway = gwMa
ipfsNodeConfig.config.Addresses.API = apiMa
const configChanges = {
customGatewayUrl: httpGateway,
ipfsApiUrl: httpApi,
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
}
// update current runtime config (in place, effective without restart)
Object.assign(opts, configChanges)
// update user config in storage (effective on next run)
await browser.storage.local.set(configChanges)
}
}

exports.destroy = async function () {
log('destroy: embedded js-ipfs+chrome.sockets')
log('destroy: embedded:chromesockets')

/*
if (httpServer) {
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/lib/ipfs-client/embedded.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ exports.init = function init (opts) {
reject(error)
})
node.once('ready', async () => {
node.on('start', () => {
node.once('start', () => {
resolve(node)
})
node.on('error', error => {
Expand Down
1 change: 1 addition & 0 deletions add-on/src/lib/ipfs-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const embeddedWithChromeSockets = require('./embedded-chromesockets')
let client

async function initIpfsClient (opts) {
log('init ipfs client')
await destroyIpfsClient()
switch (opts.ipfsNodeType) {
case 'embedded':
Expand Down
21 changes: 14 additions & 7 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use strict'
/* eslint-env browser, webextensions */

const debug = require('debug')
const log = debug('ipfs-companion:main')
log.error = debug('ipfs-companion:main:error')

const browser = require('webextension-polyfill')
const toMultiaddr = require('uri-to-multiaddr')
const { optionDefaults, storeMissingOptions, migrateOptions } = require('./options')
Expand Down Expand Up @@ -37,7 +41,9 @@ module.exports = async function init () {
const browserActionPortName = 'browser-action-port'

try {
log('init')
await migrateOptions(browser.storage.local)
await storeMissingOptions(await browser.storage.local.get(), optionDefaults, browser.storage.local)
const options = await browser.storage.local.get(optionDefaults)
runtime = await createRuntimeChecks(browser)
state = initState(options)
Expand Down Expand Up @@ -68,16 +74,13 @@ module.exports = async function init () {
modifyRequest = createRequestModifier(getState, dnslinkResolver, ipfsPathValidator, runtime)
ipfsProxy = createIpfsProxy(getIpfs, getState)
ipfsProxyContentScript = await registerIpfsProxyContentScript()
log('register all listeners')
registerListeners()
await setApiStatusUpdateInterval(options.ipfsApiPollMs)
await storeMissingOptions(
await browser.storage.local.get(),
optionDefaults,
browser.storage.local
)
log('init done')
await showPendingLandingPages()
} catch (error) {
console.error('Unable to initialize addon due to error', error)
log.error('Unable to initialize addon due to error', error)
if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
throw error
}
Expand Down Expand Up @@ -596,6 +599,7 @@ module.exports = async function init () {

async function onStorageChange (changes, area) {
let shouldRestartIpfsClient = false
let shouldStopIpfsClient = false

for (let key in changes) {
const change = changes[key]
Expand All @@ -608,6 +612,7 @@ module.exports = async function init () {
state[key] = change.newValue
ipfsProxyContentScript = await registerIpfsProxyContentScript()
shouldRestartIpfsClient = true
shouldStopIpfsClient = !state.active
break
case 'ipfsNodeType':
case 'ipfsNodeConfig':
Expand Down Expand Up @@ -656,8 +661,9 @@ module.exports = async function init () {
}
}

if (shouldRestartIpfsClient) {
if ((state.active && shouldRestartIpfsClient) || shouldStopIpfsClient) {
try {
log('stoping ipfs client due to config changes', changes)
await destroyIpfsClient()
} catch (err) {
console.error('[ipfs-companion] Failed to destroy IPFS client', err)
Expand All @@ -669,6 +675,7 @@ module.exports = async function init () {
if (!state.active) return

try {
log('starting ipfs client with the new config')
ipfs = await initIpfsClient(state)
} catch (err) {
console.error('[ipfs-companion] Failed to init IPFS client', err)
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/lib/on-installed.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ exports.showPendingLandingPages = async () => {
const hint = await browser.storage.local.get('showLandingPage')
switch (hint.showLandingPage) {
case 'onInstallWelcome':
await browser.storage.local.remove('showLandingPage')
// TODO:restore await browser.storage.local.remove('showLandingPage')
return browser.tabs.create({
url: '/dist/landing-pages/welcome/index.html'
})
Expand Down
67 changes: 41 additions & 26 deletions add-on/src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,24 @@ exports.optionDefaults = Object.freeze({
preloadAtPublicGateway: true,
catchUnhandledProtocols: true,
displayNotifications: true,
customGatewayUrl: 'http://127.0.0.1:8080',
ipfsApiUrl: 'http://127.0.0.1:5001',
customGatewayUrl: buildCustomGatewayUrl(),
ipfsApiUrl: buildIpfsApiUrl(),
ipfsApiPollMs: 3000,
ipfsProxy: true // window.ipfs
})

function buildCustomGatewayUrl () {
// TODO: make more robust (sync with buildDefaultIpfsNodeConfig)
const port = hasChromeSocketsForTcp() ? 9091 : 8080
return `http://127.0.0.1:${port}`
}

function buildIpfsApiUrl () {
// TODO: make more robust (sync with buildDefaultIpfsNodeConfig)
const port = hasChromeSocketsForTcp() ? 5003 : 5001
return `http://127.0.0.1:${port}`
}

function buildDefaultIpfsNodeType () {
// Right now Brave is the only vendor giving us access to chrome.sockets
return hasChromeSocketsForTcp() ? 'embedded:chromesockets' : 'external'
Expand All @@ -37,36 +49,40 @@ function buildDefaultIpfsNodeConfig () {
}
}
if (hasChromeSocketsForTcp()) {
// config.config.Addresses.API = '/ip4/127.0.0.1/tcp/5002'
config.config.Addresses.API = '' // disable API port
config.config.Addresses.Gateway = '/ip4/127.0.0.1/tcp/8080'
// TODO: make more robust (sync with buildCustomGatewayUrl and buildIpfsApiUrl)
// embedded node should use different ports to make it easier
// for people already running regular go-ipfs and js-ipfs on standard ports
config.config.Addresses.API = '/ip4/127.0.0.1/tcp/5003'
config.config.Addresses.Gateway = '/ip4/127.0.0.1/tcp/9091'
/*
(Sidenote on why we need API for Web UI)
Gateway can run without API port,
but Web UI does not use window.ipfs due to sandboxing atm.
If Web UI is able to use window.ipfs, then we can remove API port.
Disabling API is as easy as:
config.config.Addresses.API = ''
*/
}
return JSON.stringify(config, null, 2)
}

// `storage` should be a browser.storage.local or similar
exports.storeMissingOptions = (read, defaults, storage) => {
exports.storeMissingOptions = async (read, defaults, storage) => {
const requiredKeys = Object.keys(defaults)
const changes = new Set()
requiredKeys.map(key => {
// limit work to defaults and missing values
const changes = {}
for (let key of requiredKeys) {
// limit work to defaults and missing values, skip values other than defaults
if (!read.hasOwnProperty(key) || read[key] === defaults[key]) {
changes.add(new Promise((resolve, reject) => {
storage.get(key).then(data => {
if (!data[key]) { // detect and fix key without value in storage
let option = {}
option[key] = defaults[key]
storage.set(option)
.then(data => { resolve(`updated:${key}`) })
.catch(error => { reject(error) })
} else {
resolve(`nochange:${key}`)
}
})
}))
const data = await storage.get(key)
if (!data.hasOwnProperty(key)) { // detect and fix key without value in storage
changes[key] = defaults[key]
}
}
})
return Promise.all(changes)
}
// save all in bulk
await storage.set(changes)
return changes
}

function normalizeGatewayURL (url) {
Expand Down Expand Up @@ -118,10 +134,9 @@ exports.migrateOptions = async (storage) => {
if (ipfsNodeType === 'embedded' && hasChromeSocketsForTcp()) {
console.log(`[ipfs-companion] migrating ipfsNodeType to 'embedded:chromesockets'`)
// Overwrite old config
const ipfsNodeConfig = JSON.parse(exports.optionDefaults.ipfsNodeConfig)
await storage.set({
ipfsNodeType: 'embedded:chromesockets',
ipfsNodeConfig: JSON.stringify(ipfsNodeConfig, null, 2)
ipfsNodeConfig: buildDefaultIpfsNodeConfig()
})
}
}
5 changes: 3 additions & 2 deletions add-on/src/options/forms/gateways-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function gatewaysForm ({
const onNoRedirectHostnamesChange = onOptionChange('noRedirectHostnames', hostTextToArray)
const mixedContentWarning = !secureContextUrl.test(customGatewayUrl)
const supportRedirectToCustomGateway = ipfsNodeType !== 'embedded'
const allowChangeOfCustomGateway = ipfsNodeType !== 'embedded:chromesockets'

return html`
<form>
Expand All @@ -47,7 +48,7 @@ function gatewaysForm ({
onchange=${onPublicGatewayUrlChange}
value=${publicGatewayUrl} />
</div>
${supportRedirectToCustomGateway ? html`
${supportRedirectToCustomGateway && allowChangeOfCustomGateway ? html`
<div>
<label for="customGatewayUrl">
<dl>
Expand All @@ -66,7 +67,7 @@ function gatewaysForm ({
spellcheck="false"
title="Enter URL without any sub-path"
onchange=${onCustomGatewayUrlChange}
${ipfsNodeType !== 'external' ? 'disabled' : ''}
${allowChangeOfCustomGateway ? '' : 'disabled'}
value=${customGatewayUrl} />
</div>
Expand Down
Loading

0 comments on commit 2b63636

Please sign in to comment.