Skip to content

Commit

Permalink
feat(migration): adjust BackendProcess, main.dev.js & App.ts so…
Browse files Browse the repository at this point in the history
… we can coordinate migrations! (#571)

* refactor(backend.js): emit messages to renderer process during backend launch

We refactor the `BackendProcess` class to have specific methods that the main process can use to orchestrate launching the backend app
Because we can only emit events to the renderer process in the main process (so no way to emit them in `BackendProcess`, unless we pass the `mainWindow` down to the `BackendProcess` class) this exposes a bunch of launching functionality to the main process (it was previously all hidden in the `backendProcess.maybeStartup()` function)

In future refactors, am definitely open to pushing `mainWindow` down to `BackendProcess` and dealing with all aspects down there. However, as a first iteration I think this is fine.

The logic for this the start up goes like this:
- if a backend process is already running, error. Catch the error & send a message to the renderer that a qri backend is already running
- check if we have a compatible backend version. If it's too low, send a message to the renderer that the version is incompatible
- check if we need a migration - if we do, send a message to the renderer that we are migrating
- launch the backend

* refactor(ui reducer): add `bootupComponent` as field on `state.ui`

- add `BootupComponentType`
- `state.ui.bootupComponet` defaults to 'loading'
- add `UI_SET_BOOTUP_COMPONENT` action type
- add `setBootstrapComponent` action
- add `selectBootstrapComponent` selector

* refactor(`main`): need to wait for app to be loaded before coordinating

Both the renderer process and main process need to be on before we can successfully coordinate behavior.

`App` component now emits an `app-fully-loaded` event.

Moved the backend launching process into the ipcMain `app-fully-loaded` event handler. This also means that whenever the app re-loads, we check for a backend connection, and if we don't find one we launch again. Which I think is a nice pick up!

Downside is we get slightly longer loading time in dev mode (since it takes a bit to compile the app), but no discernible changes in prod mode!

* refactor(backend): saner `checkNeedsMigration` function

now that the backend flags/expectations have changed, we can run any comand to see if it exits with code `2` (which indicates that a migration is needed)

* refactor(backend.js): use correct flags in `qri connect` & use `qri config` as migration check
  • Loading branch information
ramfox authored Jul 16, 2020
1 parent 866a9e4 commit 41cf068
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 31 deletions.
12 changes: 10 additions & 2 deletions app/actions/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
UI_SET_DATASET_DIR_PATH,
UI_SET_EXPORT_PATH,
UI_SET_DETAILS_BAR,
UI_SET_IMPORT_FILE_DETAILS
UI_SET_IMPORT_FILE_DETAILS,
UI_SET_BOOTUP_COMPONENT
} from '../reducers/ui'

import { ToastType } from '../models/store'
import { ToastType, BootupComponentType } from '../models/store'
import { Modal, ModalType } from '../models/modals'
import { Details } from '../models/details'

Expand Down Expand Up @@ -100,3 +101,10 @@ export const setImportFileDetails = (fileName: string, fileSize: number) => {
fileSize
}
}

export const setBootupComponent = (component: BootupComponentType) => {
return {
type: UI_SET_BOOTUP_COMPONENT,
component
}
}
110 changes: 100 additions & 10 deletions app/backend.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
const log = require('electron-log')
const childProcess = require('child_process')
const fs = require('fs')
const yaml = require('js-yaml')
const path = require('path')
const os = require('os')
const http = require('http')
const { dialog } = require('electron')

const lowestCompatibleBackend = [0, 9, 9]

// BackendProcess runs the qri backend binary in connected'ed mode, to handle api requests.
class BackendProcess {
constructor () {
this.qriBinPath = null
this.process = null
this.debugLogPath = null
this.backendVer = null

// Default to writing to stdout & stderr
this.out = process.stdout
this.err = process.stderr
this.in = process.in
this.err = process.stderr;

[
'setQriBinPath',
'setBackendVer',
'standardRepoPath',
'checkNoActiveBackendProcess',
'checkBackendCompatibility',
'checkNeedsMigration',
'launchProcess'
].forEach((m) => { this[m] = this[m].bind(this) })


try {
// Create a log whose filename contains the current day.
const nowTime = new Date();
Expand All @@ -41,9 +58,12 @@ class BackendProcess {
detail: err
})
}
this.setQriBinPath()
this.setBackendVer()
}

maybeStartup () {
// running this function will ensure that a qriBinPath exists
setQriBinPath () {
// In development node, use installed qri binary
if (process.env.NODE_ENV === 'development') {
let processResult = childProcess.execSync('which qri');
Expand All @@ -59,12 +79,17 @@ class BackendProcess {
}

if (!this.qriBinPath) {
log.warn('no qri bin path found, backend launch')
return
log.warn('no qri bin path found, backend launch failed')
throw new Error("no qri bin path found, backend launch failed")
}
// Run the binary if it is found
log.info(`found qri binary at path: ${this.qriBinPath}`)
this.launchProcess()
}

setBackendVer () {
let processResult = childProcess.execSync(`"${this.qriBinPath}" version`)
this.backendVer = processResult.toString().trim()
log.info("qri backend version", this.backendVer)

}

close () {
Expand All @@ -74,16 +99,66 @@ class BackendProcess {
}
}

standardRepoPath() {
var qriRepoPath = process.env.QRI_PATH
if (qriRepoPath === "") {
home = os.homedir()
qriRepoPath = path.join(home, ".qri")
}

log.info(`QRI_PATH is ${qriRepoPath}`)
return qriRepoPath
}

async checkNoActiveBackendProcess() {
const healthCheck = async () => {
return new Promise((res, rej) => {
http.get('http://localhost:2503/health', (data) => {
res(true)
}).on('error', (e) => {
res(false)
})
})
}

var isQriRunning = await healthCheck()
if (isQriRunning) {
throw new Error("backend-already-running")
}
return
}

async checkBackendCompatibility () {
log.info(`checking to see if given backend version ${this.backendVer} is compatible with expected version ${lowestCompatibleBackend.join(".")}`)
let compatible = false
try {
let ver = this.backendVer
if (this.backendVer.indexOf("-dev") !== -1) {
ver = ver.slice(0, this.backendVer.indexOf("-dev"))
}
ver = ver.split(".").map((i) => parseInt(i))
compatible = lowestCompatibleBackend.every((val, i) => {
if (val <= ver[i]) {
return true
}
return false
})
} catch (e) {
throw e
}
if (!compatible) {
throw new Error("incompatible-backend")
}
}

launchProcess () {
try {
let processResult = childProcess.execSync(`"${this.qriBinPath}" version`)
let qriBinVersion = processResult.toString().trim();
this.process = childProcess.spawn(this.qriBinPath, ['connect', '--setup', '--log-all'], { stdio: ['ignore', this.out, this.err] })
this.process = childProcess.spawn(this.qriBinPath, ['connect', '--migrate', '--log-all', '--setup', '--no-prompt'], { stdio: ['ignore', this.out, this.err] })
this.process.on('error', (err) => { this.handleEvent('error', err) })
this.process.on('exit', (err) => { this.handleEvent('exit', err) })
this.process.on('close', (err) => { this.handleEvent('close', err) })
this.process.on('disconnect', (err) => { this.handleEvent('disconnect', err) })
log.info(`starting up qri backend version ${qriBinVersion}`)
log.info(`starting up qri backend version ${this.backendVer}`)
} catch (err) {
log.error(`starting background process: ${err}`)
}
Expand All @@ -92,11 +167,26 @@ class BackendProcess {
handleEvent (kind, err) {
if (err) {
log.error(`event ${kind} from backend: ${err}`)
throw err
} else {
log.warn(`event ${kind} from backend`)
}
}

checkNeedsMigration() {
log.info("checking for backend migrations")
try {
childProcess.execSync(`"${this.qriBinPath}" config get`)
} catch (err) {
// status code 2 means we need to run a migration
if (err.status == 2) {
return true
}
log.error(`error checking for migration: ${err}`)
}
return false
}

findQriBin (pathList) {
for (let i = 0; i < pathList.length; i++) {
let binPath = path.join(pathList[i], '/backend/qri')
Expand Down
97 changes: 83 additions & 14 deletions app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ import { DEFAULT_POLL_INTERVAL } from '../constants'
import { history } from '../store/configureStore.development'
import { ApiAction } from '../store/api'
import { Modal, ModalType } from '../models/modals'
import { Selections, ApiConnection } from '../models/store'
import { Selections, ApiConnection, BootupComponentType } from '../models/store'
import { Session } from '../models/session'

// import util funcs
import { connectComponentToProps } from '../utils/connectComponentToProps'

// import actions
import { setModal } from '../actions/ui'
import { setModal, setBootupComponent } from '../actions/ui'
import { pingApi } from '../actions/api'
import { bootstrap } from '../actions/session'

// import selections
import { selectSelections, selectModal, selectApiConnection, selectSession } from '../selections'
import { selectSelections, selectModal, selectApiConnection, selectSession, selectBootupComponent } from '../selections'

// import components
import Toast from './Toast'
Expand All @@ -34,10 +34,14 @@ import AppError from './AppError'
import AppLoading from './AppLoading'
import Modals from './modals/Modals'
import Routes from '../routes'
import MigratingBackend from './MigratingBackend'
import MigrationFailed from './MigrationFailed'
import IncompatibleBackend from './IncompatibleBackend'

// declare interface for props
export interface AppProps {
loading: boolean
bootupComponent: BootupComponentType
session: Session
selections: Selections
apiConnection?: ApiConnection
Expand All @@ -46,6 +50,7 @@ export interface AppProps {

push: (path: string) => void
setModal: (modal: Modal) => Action
setBootupComponent: (component: BootupComponentType) => Action

bootstrap: () => Promise<ApiAction>
pingApi: () => Promise<ApiAction>
Expand Down Expand Up @@ -73,6 +78,14 @@ class AppComponent extends React.Component<AppProps, AppState> {
this.handleReload = this.handleReload.bind(this)
this.handleSetDebugLogPath = this.handleSetDebugLogPath.bind(this)
this.handleExportDebugLog = this.handleExportDebugLog.bind(this)
this.handleIncompatibleBackend = this.handleIncompatibleBackend.bind(this)
this.handleMigratingBackend = this.handleMigratingBackend.bind(this)
this.handleMigrationFailure = this.handleMigrationFailure.bind(this)
this.handleStartingBackend = this.handleStartingBackend.bind(this)
}

private handleStartingBackend () {
console.log("started backend message received")
}

private handleCreateDataset () {
Expand Down Expand Up @@ -112,6 +125,18 @@ class AppComponent extends React.Component<AppProps, AppState> {
fs.copyFileSync(this.state.debugLogPath, exportFilename)
}

private handleIncompatibleBackend (_: Electron.IpcRendererEvent, ver: string) {
this.props.setBootupComponent(ver)
}

private handleMigratingBackend () {
this.props.setBootupComponent('migrating')
}

private handleMigrationFailure () {
this.props.setBootupComponent('migrationFailure')
}

componentDidMount () {
// handle ipc events from electron menus
ipcRenderer.on('create-dataset', this.handleCreateDataset)
Expand All @@ -120,6 +145,12 @@ class AppComponent extends React.Component<AppProps, AppState> {
ipcRenderer.on('set-debug-log-path', this.handleSetDebugLogPath)
ipcRenderer.on('export-debug-log', this.handleExportDebugLog)
ipcRenderer.on('reload', this.handleReload)
ipcRenderer.on('incompatible-backend', this.handleIncompatibleBackend)
ipcRenderer.on('migrating-backend', this.handleMigratingBackend)
ipcRenderer.on('migration-failed', this.handleMigrationFailure)
ipcRenderer.on("starting-backend", this.handleStartingBackend)

ipcRenderer.send("app-fully-loaded")

setInterval(() => {
if (this.props.apiConnection !== 1) {
Expand All @@ -138,6 +169,10 @@ class AppComponent extends React.Component<AppProps, AppState> {
ipcRenderer.removeListener('history-push', this.handlePush)
ipcRenderer.removeListener('set-debug-log-path', this.handleSetDebugLogPath)
ipcRenderer.removeListener('reload', this.handleReload)
ipcRenderer.removeListener('incompatible-backend', this.handleIncompatibleBackend)
ipcRenderer.removeListener('migrating-backend', this.handleMigratingBackend)
ipcRenderer.removeListener('migration-failed', this.handleMigrationFailure)
ipcRenderer.removeListener("starting-backend", this.handleStartingBackend)
}

componentDidUpdate (prevProps: AppProps) {
Expand All @@ -151,20 +186,52 @@ class AppComponent extends React.Component<AppProps, AppState> {
}

render () {
const { apiConnection, modal, loading } = this.props
const { apiConnection, modal, loading, bootupComponent } = this.props

if (loading) {
return (
<CSSTransition
in={loading}
classNames="fade"
component="div"
timeout={1000}
mountOnEnter
unmountOnExit
>
<AppLoading />
</CSSTransition>
<>
<CSSTransition
in={bootupComponent === 'loading'}
classNames="fade"
component="div"
timeout={1000}
mountOnEnter
unmountOnExit
>
<AppLoading />
</CSSTransition>
<CSSTransition
in={bootupComponent === 'migrating'}
classNames="fade"
component="div"
timeout={1000}
mountOnEnter
unmountOnExit
>
<MigratingBackend />
</CSSTransition>
<CSSTransition
in={bootupComponent === 'migrationFailure'}
classNames="fade"
component="div"
timeout={1000}
mountOnEnter
unmountOnExit
>
<MigrationFailed />
</CSSTransition>
<CSSTransition
in={bootupComponent !== 'loading' && bootupComponent !== 'migrating' && bootupComponent !== 'migrationFailure'}
classNames="fade"
component="div"
timeout={1000}
mountOnEnter
unmountOnExit
>
<IncompatibleBackend incompatibleVersion={bootupComponent} />
</CSSTransition>
</>
)
}

Expand Down Expand Up @@ -228,6 +295,7 @@ export default connectComponentToProps(
selections: selectSelections(state),
apiConnection,
modal: selectModal(state),
bootupComponent: selectBootupComponent(state),
...ownProps
}
},
Expand All @@ -236,6 +304,7 @@ export default connectComponentToProps(
push,
setModal,
bootstrap,
setBootupComponent,
pingApi
}
)
Loading

0 comments on commit 41cf068

Please sign in to comment.