Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into NodeGuy/#304
Browse files Browse the repository at this point in the history
  • Loading branch information
faboweb committed Apr 13, 2018
2 parents 42b4e3f + ebc546c commit c3be0d4
Show file tree
Hide file tree
Showing 18 changed files with 809 additions and 376 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
* Improved performance of amountBonded in LiDelegate.vue./ @nylira
* Prevented user from going to PageBond if they don't have any atoms. @nylira
* Hid the bonding interface on PageDelegates if the user doesn't have any atoms. @nylira
* Improved error handling by shutting down the application when the are unhandled errors in the main thread. @faboweb
* Improved error handling by shutting down the application when there are unhandled errors in the main thread. @faboweb

## [0.4.0] - 2018-01-31

Expand Down Expand Up @@ -89,6 +89,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

* Added `mvp-features.md` to documentation. @nylira
* Added full page error @nylira
* Added receive button and receive modal @jolesbi
* The validator hash now has to be approved by the user for security @faboweb

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ A list of all environment variables and their purpose:
| COSMOS_DEVTOOLS | 'true', 'false' | 'false' | Open the debug panel in the electron view |
| ELECTRON_ENABLE_LOGGING | 'true', 'false' | 'false' | Redirect the browser view console output to the console |
| PREVIEW | 'true', 'false' | 'true' if NODE_ENV 'development' | Show/Hide features that are in development |
| COSMOS_E2E_KEEP_OPEN | 'true', 'false' | 'false' | Keep the Window open in local E2E test to see the state in which the application broke. |

### FAQ

Expand Down
212 changes: 140 additions & 72 deletions app/src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ let { join } = require("path")
let { spawn } = require("child_process")
let home = require("user-home")
let semver = require("semver")
// this dependency is wrapped in a file as it was not possible to mock the import with jest any other way
let event = require("event-to-promise")
let toml = require("toml")
let axios = require("axios")
let Raven = require("raven")
Expand All @@ -22,8 +20,8 @@ let lcdProcess
let streams = []
let nodeIP
let connecting = true
let crashingError = null
let seeds = null
let booted = false

const root = require("../root.js")
const networkPath = require("../network.js").path
Expand Down Expand Up @@ -72,16 +70,19 @@ function expectCleanExit(process, errorMessage = "Process exited unplanned") {
return new Promise((resolve, reject) => {
process.on("exit", code => {
if (code !== 0 && !shuttingDown) {
throw new Error(errorMessage)
reject(Error(errorMessage))
}
resolve()
})
})
}

function handleCrash(error) {
crashingError = error
mainWindow.webContents.send("error", error)
afterBooted(() => {
if (mainWindow) {
mainWindow.webContents.send("error", error)
}
})
}

function shutdown() {
Expand All @@ -98,7 +99,9 @@ function shutdown() {

return Promise.all(
streams.map(stream => new Promise(resolve => stream.close(resolve)))
)
).then(() => {
log("[SHUTDOWN] Voyager has shutdown")
})
}

function createWindow() {
Expand All @@ -114,7 +117,8 @@ function createWindow() {
webPreferences: { webSecurity: false }
})

mainWindow.loadURL(winURL + "?node=" + nodeIP + "&lcd_port=" + LCD_PORT)
// start vue app
mainWindow.loadURL(winURL + "?lcd_port=" + LCD_PORT)

if (DEV || JSON.parse(process.env.COSMOS_DEVTOOLS || "false")) {
mainWindow.webContents.openDevTools()
Expand Down Expand Up @@ -173,7 +177,6 @@ function startProcess(name, args, env) {
)
child.on("error", async function(err) {
if (!(shuttingDown && err.code === "ECONNRESET")) {
await new Promise(resolve => Raven.captureException(err, resolve))
// if we throw errors here, they are not handled by the main process
console.error(
"[Uncaught Exception] Child",
Expand All @@ -182,6 +185,8 @@ function startProcess(name, args, env) {
err
)
handleCrash(err)

Raven.captureException(err)
}
})
return child
Expand All @@ -201,27 +206,36 @@ app.on("ready", () => createWindow())

// start lcd REST API
async function startLCD(home, nodeIP) {
log("startLCD", home)
let child = startProcess(SERVER_BINARY, [
"rest-server",
"--port",
LCD_PORT,
"--home",
home,
"--node",
nodeIP
// '--trust-node'
])
logProcess(child, join(home, "lcd.log"))

while (true) {
if (shuttingDown) break

let data = await event(child.stderr, "data")
if (data.toString().includes("Serving on")) break
}
return new Promise((resolve, reject) => {
log("startLCD", home)
let child = startProcess(SERVER_BINARY, [
"rest-server",
"--port",
LCD_PORT,
"--home",
home,
"--node",
nodeIP
// '--trust-node'
])
logProcess(child, join(home, "lcd.log"))

return child
// XXX why the hell stderr?!?!?!?
child.stderr.on("data", data => {
if (data.includes("Serving on")) resolve(child)
})
child.on("exit", () => {
reject()
afterBooted(() => {
if (mainWindow) {
mainWindow.webContents.send(
"error",
Error("The Gaia REST-server (LCD) exited unplanned")
)
}
})
})
})
}

async function getGaiaVersion() {
Expand All @@ -242,33 +256,94 @@ function exists(path) {
}
}

function handleHashVerification(nodeHash) {
function removeListeners() {
ipcMain.removeAllListeners("hash-disapproved")
ipcMain.removeAllListeners("hash-approved")
}
return new Promise((resolve, reject) => {
ipcMain.on("hash-approved", (event, hash) => {
if (hash === nodeHash) {
resolve()
} else {
reject()
}
})
ipcMain.on("hash-disapproved", (event, hash) => {
reject()
})
}).finally(removeListeners)
}

async function initLCD(chainId, home, node) {
// fs.ensureDirSync(home)
// `gaia client init` to generate config, trust seed
let child = startProcess(SERVER_BINARY, [
"client",
"init",
"--home",
home,
"--chain-id",
chainId,
"--node",
node
// '--trust-node'
])
child.stdout.on("data", data => {
let hashMatch = /\w{40}/g.exec(data)
if (hashMatch) {
log("approving hash", hashMatch[0])
if (shuttingDown) return
// answer 'y' to the prompt about trust seed. we can trust this is correct
// since the LCD is talking to our own full node
child.stdin.write("y\n")
}
// let the user in the view approve the hash we get from the node
return new Promise((resolve, reject) => {
// `gaia client init` to generate config
let child = startProcess(SERVER_BINARY, [
"client",
"init",
"--home",
home,
"--chain-id",
chainId,
"--node",
node
])

child.stdout.on("data", async data => {
let hashMatch = /\w{40}/g.exec(data)
if (hashMatch) {
handleHashVerification(hashMatch[0])
.then(
async () => {
log("approved hash", hashMatch[0])
if (shuttingDown) return
// answer 'y' to the prompt about trust seed. we can trust this is correct
// since the LCD is talking to our own full node
child.stdin.write("y\n")

expectCleanExit(child, "gaia init exited unplanned").then(
resolve,
reject
)
},
() => {
// kill process as we will spin up a new init process
child.kill("SIGTERM")

if (shuttingDown) return

// select a new node to try out
nodeIP = pickNode(seeds)

initLCD(chainId, home, nodeIP).then(resolve, reject)
}
)
.catch(reject)

// execute after registering handlers via handleHashVerification so that in the synchronous test they are available to answer the request
afterBooted(() => {
mainWindow.webContents.send("approve-hash", hashMatch[0])
})
}
})
})
await expectCleanExit(child, "gaia init exited unplanned")
}

// this function will call the passed in callback when the view is booted
// the purpose is to send events to the view thread only after it is ready to receive those events
// if we don't do this, the view thread misses out on those (i.e. an error that occures before the view is ready)
function afterBooted(cb) {
if (booted) {
cb()
} else {
ipcMain.on("booted", event => {
cb()
})
}
}

/*
* log to file
*/
Expand Down Expand Up @@ -306,15 +381,13 @@ if (!TEST) {
process.on("exit", shutdown)
// on uncaught exceptions we wait so the sentry event can be sent
process.on("uncaughtException", async function(err) {
await sleep(1000)
logError("[Uncaught Exception]", err)
await new Promise(resolve => Raven.captureException(err, resolve))
Raven.captureException(err)
handleCrash(err)
})
process.on("unhandledRejection", async function(err) {
await sleep(1000)
logError("[Unhandled Promise Rejection]", err)
await new Promise(resolve => Raven.captureException(err, resolve))
Raven.captureException(err)
handleCrash(err)
})
}
Expand All @@ -337,16 +410,10 @@ function handleIPC() {
ipcMain.on("successful-launch", () => {
console.log("[START SUCCESS] Vue app successfuly started")
})
ipcMain.on("reconnect", function(event) {
return reconnect(seeds)
})
ipcMain.on("booted", event => {
// if the webcontent shows after we have connected to a node or produced, we need to send those events again
if (crashingError) {
event.sender.send("error", crashingError)
} else if (!connecting && nodeIP) {
event.sender.send("connected", nodeIP)
}
ipcMain.on("reconnect", () => reconnect(seeds))
ipcMain.on("booted", () => {
log("View has booted")
booted = true
})
ipcMain.on("error-collection", (event, optin) => {
Raven.uninstall()
Expand Down Expand Up @@ -396,12 +463,14 @@ async function connect(seeds, nodeIP) {
lcdProcess = await startLCD(lcdHome, nodeIP)
log("gaia server ready")

console.log("connected")

mainWindow.webContents.send("connected", nodeIP)
afterBooted(() => {
log("Signaling connected node")
mainWindow.webContents.send("connected", nodeIP)
})

connecting = false

// signal new node to view
return nodeIP
}

Expand Down Expand Up @@ -539,17 +608,16 @@ async function main() {
if (seeds.length === 0) {
throw new Error("No seeds specified in config.toml")
}

// choose one random node to start from
nodeIP = pickNode(seeds)

let _lcdInitialized = await lcdInitialized(join(root, "lcd"))
console.log("LCD is", _lcdInitialized ? "" : "not", "initialized")
log("LCD is" + (_lcdInitialized ? "" : " not") + " initialized")
if (init || !_lcdInitialized) {
log(`Trying to initialize lcd with remote node ${nodeIP}`)
await initLCD(chainId, lcdHome, nodeIP)
}

console.log("connecting")

await connect(seeds, nodeIP)
}
module.exports = main()
Expand Down
7 changes: 6 additions & 1 deletion app/src/renderer/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
session(v-else)
notifications(:notifications='notifications' theme='cosmos')
modal-error(v-if='config.modals.error.active' :body='config.modals.error.message')
modal-lcd-approval(v-if='approvalRequired' :hash='approvalRequired')
</template>

<script>
Expand All @@ -21,6 +22,7 @@ import ModalError from "common/NiModalError"
import ModalHelp from "common/NiModalHelp"
import ModalReceive from "common/NiModalReceive"
import Session from "common/NiSession"
import ModalLcdApproval from "common/NiModalLCDApproval"
import store from "./vuex/store"
export default {
name: "app",
Expand All @@ -29,11 +31,14 @@ export default {
AppFooter,
ModalError,
ModalHelp,
ModalLcdApproval,
ModalReceive,
Notifications,
Session
},
computed: { ...mapGetters(["notifications", "config", "themes"]) },
computed: {
...mapGetters(["notifications", "config", "themes", "approvalRequired"])
},
mounted() {
this.$store.commit("updateTheme", this.themes.active)
},
Expand Down
Loading

0 comments on commit c3be0d4

Please sign in to comment.