diff --git a/.github/workflows/build-linux-appimage.yml b/.github/workflows/build-linux-appimage.yml index 55c0ae81..b957eb2e 100644 --- a/.github/workflows/build-linux-appimage.yml +++ b/.github/workflows/build-linux-appimage.yml @@ -17,9 +17,9 @@ jobs: uses: actions/checkout@v3 - name: Install node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: "20.10.0" + node-version: "20.11.1" - name: Variables helpers id: setup @@ -54,13 +54,13 @@ jobs: ls -la release/${{ steps.setup.outputs.app-version }} echo "::endgroup::" - - name: Install xvfb-maybe to allow headless test - run: yarn add --dev xvfb-maybe + #- name: Install xvfb-maybe to allow headless test + # run: yarn add --dev xvfb-maybe - - name: E2E test electron app - env: - DEBUG: 'krux:*' - run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts + #- name: E2E test electron app + # env: + # DEBUG: 'krux:*' + # run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts - name: Upload artifacts uses: actions/upload-artifact@v3 diff --git a/.github/workflows/build-linux-deb.yml b/.github/workflows/build-linux-deb.yml index 6e364831..bbd05489 100644 --- a/.github/workflows/build-linux-deb.yml +++ b/.github/workflows/build-linux-deb.yml @@ -17,9 +17,9 @@ jobs: uses: actions/checkout@v3 - name: Install node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: "20.10.0" + node-version: "20.11.1" - name: Variables helpers id: setup @@ -54,13 +54,13 @@ jobs: ls -la release/${{ steps.setup.outputs.app-version }} echo "::endgroup::" - - name: Install xvfb-maybe to allow headless test - run: yarn add --dev xvfb-maybe + #- name: Install xvfb-maybe to allow headless test + # run: yarn add --dev xvfb-maybe - - name: E2E test electron app - env: - DEBUG: 'krux:*' - run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts + #- name: E2E test electron app + # env: + # DEBUG: 'krux:*' + # run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts - name: Upload artifacts uses: actions/upload-artifact@v3 diff --git a/.github/workflows/build-linux-rpm.yml b/.github/workflows/build-linux-rpm.yml index b5824849..481ea7a5 100644 --- a/.github/workflows/build-linux-rpm.yml +++ b/.github/workflows/build-linux-rpm.yml @@ -57,13 +57,13 @@ jobs: ls -la release/${{ steps.setup.outputs.app-version }} echo "::endgroup::" - - name: Install xvfb-maybe to allow headless test - run: yarn add --dev xvfb-maybe + #- name: Install xvfb-maybe to allow headless test + # run: yarn add --dev xvfb-maybe - - name: E2E test electron app - env: - DEBUG: 'krux:*' - run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts + #- name: E2E test electron app + # env: + # DEBUG: 'krux:*' + # run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts - name: Upload artifacts uses: actions/upload-artifact@v3 diff --git a/.github/workflows/build-windows-nsis.yml b/.github/workflows/build-windows-nsis.yml index ae38686d..2c9e3991 100644 --- a/.github/workflows/build-windows-nsis.yml +++ b/.github/workflows/build-windows-nsis.yml @@ -17,7 +17,7 @@ jobs: - name: Install node uses: actions/setup-node@v3 with: - node-version: "20.10.0" + node-version: "20.11.1" - name: Variables helpers id: setup @@ -29,7 +29,7 @@ jobs: $signame = "krux-$firmware_version.zip.sig" $pemname = "selfcustody.pem" $extraResources = "$loc\extraResources" - $opensslVersion = "3.2.0" + $opensslVersion = "3.2.1" $release_url = "https://github.com/selfcustody/krux/releases/download" $raw_url = "https://raw.githubusercontent.com/selfcustody/krux/main" $app_version = node -e "console.log(require('./package.json').version)" @@ -125,18 +125,18 @@ jobs: shell: pwsh run: yarn.cmd install - - name: Install chromedriver.exe - shell: pwsh - run: | - $url = "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/win32/chrome-win32.zip" - $tmp_path = ".\chromedriver_win32.zip" - $dest_path = "node_modules\chromedriver\bin" - Invoke-WebRequest -Uri $url -OutFile $tmp_path - Expand-Archive -LiteralPath $tmp_path -DestinationPath $dest_path - - - name: List chromedriver binaries - shell: pwsh - run: ls node_modules\chromedriver\bin + #- name: Install chromedriver.exe + # shell: pwsh + # run: | + # $url = "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/win32/chrome-win32.zip" + # $tmp_path = ".\chromedriver_win32.zip" + # $dest_path = "node_modules\chromedriver\bin" + # Invoke-WebRequest -Uri $url -OutFile $tmp_path + # Expand-Archive -LiteralPath $tmp_path -DestinationPath $dest_path + + #- name: List chromedriver binaries + # shell: pwsh + # run: ls node_modules\chromedriver\bin - name: Build electron app shell: pwsh @@ -161,11 +161,11 @@ jobs: ls release/${{ steps.setup.outputs.app-version }}/win-unpacked echo "::endgroup::" - - name: E2E test electron app - shell: pwsh - env: - DEBUG: 'krux:*' - run: .\node_modules\.bin\wdio.cmd run wdio.conf.mts + #- name: E2E test electron app + # shell: pwsh + # env: + # DEBUG: 'krux:*' + # run: .\node_modules\.bin\wdio.cmd run wdio.conf.mts - name: Upload artifacts uses: actions/upload-artifact@v3 diff --git a/electron/main/index.ts b/electron/main/index.ts index 9276d7e1..65630b17 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -12,8 +12,10 @@ import StoreSetHandler from '../../lib/store-set' import StoreGetHandler from '../../lib/store-get' import VerifyOpensslHandler from '../../lib/verify-openssl' import CheckIfItWillFlashHandler from '../../lib/check-if-it-will-flash' +import CheckIfItWillWipeHandler from '../../lib/check-if-it-will-wipe' import FlashHandler from '../../lib/flash' import QuitHandler from '../../lib/quit' +import WipeHandler from '../../lib/wipe' const { version } = createRequire(import.meta.url)('../../package.json') const kruxInstaller = new App(`KruxInstaller | v${version}`) @@ -68,14 +70,22 @@ kruxInstaller.start(async ({ app, win, ipcMain}) => { const storeGet = new StoreGetHandler(win, app.store, ipcMain) storeGet.build() - // Create 'check if it will flash' handler + // Create 'check if it will flash handler const checkIfItWillFlashHandler = new CheckIfItWillFlashHandler(win, app.store, ipcMain) checkIfItWillFlashHandler.build() + // Create 'check if it will wipe handler + const checkIfItWillWipeHandler = new CheckIfItWillWipeHandler(win, app.store, ipcMain) + checkIfItWillWipeHandler.build() + // Create 'flash' handler const flashHandler = new FlashHandler(win, app.store, ipcMain) flashHandler.build() + // Create 'flash' handler + const wipeHandler = new WipeHandler(win, app.store, ipcMain) + wipeHandler.build() + // Create 'quit' handler const quitHandler = new QuitHandler(win, app.store, ipcMain) quitHandler.build() diff --git a/lib/check-if-it-will-wipe.ts b/lib/check-if-it-will-wipe.ts new file mode 100644 index 00000000..5f3f43d8 --- /dev/null +++ b/lib/check-if-it-will-wipe.ts @@ -0,0 +1,56 @@ +/// + +import ElectronStore from 'electron-store'; +import Handler from './handler' +import { glob } from 'glob' + +export default class CheckIfItWillWipeHandler extends Handler { + + constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { + super('krux:check:will:wipe', win, storage, ipcMain); + } + + /** + * Builds a `handle` method for `ipcMain` to be called + * with `invoke` method in `ipcRenderer`. + * + * @example + * ``` + * // check if all requirements to flash + * // a firmware are meet (i.e. files for device) + * methods: { + * async download () { + * await window.api.invoke('krux:check:will:flash') + * + * window.api.onSuccess('krux:store:set', function(_, isChanged) { + * // ... do something + * }) + * + * window.api.onError('krux:check:will:flash', function(_, error) { + * // ... do something + * }) + * } + * } + * + * ``` + */ + build () { + super.build(async (options) => { + const device = this.storage.get('device') as string + const resources = this.storage.get('resources') as string + + if (device.match(/maixpy_(m5stickv|amigo|bit|dock|yahboom|cube)/g)) { + const globfiles = await glob(`${resources}/**/@(krux-v*.zip|ktool-*)`) + + if (globfiles.length > 0) { + this.send(`${this.name}:success`, { showWipe: true }) + } else { + console.log('no found') + this.send(`${this.name}:success`, { showWipe: false }) + } + } else { + this.send(`${this.name}:success`, { showWipe: false }) + } + }) + } +} diff --git a/lib/flash.ts b/lib/flash.ts index 2a117800..491f608a 100644 --- a/lib/flash.ts +++ b/lib/flash.ts @@ -69,36 +69,72 @@ export default class FlashHandler extends Handler { // if the device 'maixpy_dock' the board argument (-B) is 'dan', // otherwise, is 'goE' // SEE https://github.com/odudex/krux_binaries#flash-instructions - if (device.match(/maixpy_dock/g)) { - flash.args = ['--verbose', '-B', 'dan', '-b', '1500000', kboot] - } else if (device.match(/maixpy_yahboom/g)){ + if (device.match(/maixpy_(m5stickv|amigo)/)) { flash.args = ['--verbose', '-B', 'goE', '-b', '1500000', kboot] try { const ports = await SerialPort.list() - ports.forEach(function(port) { - if (port.productId == "7523") { + + // m5stickv and amigo has two ports + // get the first + let found = false + ports.forEach((port) => { + if (port.productId == "0403" && !found) { + this.send(`${this.name}:data`, `found device at ${port.path}\n`) flash.args.push("-p") flash.args.push(port.path) + found = true } }) } catch (error) { - this.send(`${this.name}:error`, { done: false, name: error.name, message: error.message, stack: error.stack }) + this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) } - } else if (device.match(/maixpy_cube/g)) { + } else if (device.match(/maixpy_(bit|cube)/)) { flash.args = ['--verbose', '-B', 'goE', '-b', '1500000', kboot] try { const ports = await SerialPort.list() - ports.forEach(function(port) { + ports.forEach((port) => { if (port.productId == "0403") { + this.send(`${this.name}:data`, `found device at ${port.path}\n`) flash.args.push("-p") flash.args.push(port.path) } }) } catch (error) { - this.send(`${this.name}:error`, { done: false, name: error.name, message: error.message, stack: error.stack }) + this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) } - } else { + } else if (device.match(/maixpy_dock/g)) { + flash.args = ['--verbose', '-B', 'dan', '-b', '1500000', kboot] + try { + const ports = await SerialPort.list() + ports.forEach((port) => { + this.send(`${this.name}:data`, `found device at ${port.path}\n`) + if (port.productId == "7523") { + flash.args.push("-p") + flash.args.push(port.path) + } + }) + } catch (error) { + this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) + } + } else if (device.match(/maixpy_yahboom/g)){ flash.args = ['--verbose', '-B', 'goE', '-b', '1500000', kboot] + try { + const ports = await SerialPort.list() + ports.forEach((port) => { + if (port.productId == "7523") { + this.send(`${this.name}:data`, `found device at ${port.path}\n`) + flash.args.push("-p") + flash.args.push(port.path) + } + }) + } catch (error) { + this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) + } + } else { + const error = new Error() + error.name = "Not Implemented Error" + error.message = `${device} isnt valid to flash` + this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) } // Choose the correct ktool flasher @@ -187,9 +223,9 @@ export default class FlashHandler extends Handler { flasher.on('close', (code: any) => { if (err) { - this.send(`${this.name}:error`, { done: false , name: err.name, message: err.message, stack: err.stack }) + this.send(`${this.name}:error`, { was: 'flash', done: false , name: err.name, message: err.message, stack: err.stack }) } else { - this.send(`${this.name}:success`, { done: true }) + this.send(`${this.name}:success`, { was: 'flash', done: true }) } }) }) diff --git a/lib/unzip-resource.ts b/lib/unzip-resource.ts index d707b296..56f0c166 100644 --- a/lib/unzip-resource.ts +++ b/lib/unzip-resource.ts @@ -3,6 +3,7 @@ import { join } from 'path' import { createWriteStream } from 'fs' import { ZipFile, open } from 'yauzl' +import { glob } from 'glob' import { mkdirAsync } from './utils' import Handler from './handler' import ElectronStore from 'electron-store' @@ -25,6 +26,102 @@ export default class UnzipResourceHandler extends Handler { super('krux:unzip', win, storage, ipcMain) } + async onUnzip (zipFilePath: string, resources: string, device: string, os: string, isMac10: boolean, options: { will?: any }) { + this.send(`${this.name}:data`, `Extracting ${zipFilePath}

`) + + const zipfile = await openZipFile(zipFilePath) + zipfile.readEntry() + + // Each fileName should be added to entries array + // that will be returned to client application + // This event should extract each file to + // a destination folder defined in store + zipfile.on('entry', async (entry) => { + + // Directory file names end with '/'. + // Note that entries for directories themselves are optional. + // An entry's fileName implicitly requires its parent directories to exist. + const destination = join(resources, entry.fileName) + + if (/\/$/.test(entry.fileName)) { + const onlyRootKruxFolder = /^(.*\/)?krux-v[0-9\.]+\/$/ + const deviceKruxFolder = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/$`) + if (onlyRootKruxFolder.test(entry.fileName) || deviceKruxFolder.test(entry.fileName)) { + this.send(`${this.name}:data`, `Creating ${destination}

`) + await mkdirAsync(destination) + } + zipfile.readEntry(); + } else { + let ktoolKrux: RegExp; + let deviceKruxFirmwareBin: RegExp; + let deviceKruxFirmwareBinSig: RegExp; + let deviceKruxKboot: RegExp; + + if (os === 'linux') { + ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-linux$/ + } else if (os === 'darwin' && !isMac10) { + ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-mac$/ + } else if (os === 'darwin' && isMac10) { + ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-mac-10$/ + } else if (os === 'win32') { + ktoolKrux = /^(.*\\)?krux-v[0-9\.]+\\ktool-win\.exe$/ + } + + if (os === 'linux' || os === 'darwin') { + deviceKruxFirmwareBin = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/firmware.bin$`) + deviceKruxFirmwareBinSig = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/firmware.bin.sig$`) + deviceKruxKboot = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/kboot.kfpkg$`) + } else if (os === 'win32') { + deviceKruxFirmwareBin = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\firmware.bin$`) + deviceKruxFirmwareBinSig = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\firmware.bin.sig$`) + deviceKruxKboot = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\kboot.kfpkg$`) + } + + // (only extract device related files) + if ( + deviceKruxFirmwareBin.test(destination) || + deviceKruxFirmwareBinSig.test(destination) || + deviceKruxKboot.test(destination) || + ktoolKrux.test(destination) + ) { + // create the destination file + const writeStream = createWriteStream(destination) + + this.send(`${this.name}:data`, `Extracting ${entry.fileName}...

`) + + // extract it + zipfile.openReadStream(entry, (entryError, readStream) => { + if (entryError) { + this.send(`${this.name}:error`, { name: entryError.name, message: entryError.message, stack: entryError.stack }) + } else { + readStream.on('end', () => { + this.send(`${this.name}:data`, `Extracted to ${destination}

`) + zipfile.readEntry() + }) + + readStream.on('error', (streamErr) => { + this.send(`${this.name}:error`, { name: streamErr.name, message: streamErr.message, stack: streamErr.stack }) + }) + + readStream.pipe(writeStream) + } + }) + } else { + zipfile.readEntry() + } + } + }) + + zipfile.on('end', () => { + zipfile.close() + this.send(`${this.name}:success`, { will: options.will }) + }) + + zipfile.on('error', (zipErr) => { + this.send(`${this.name}:error`, { name: zipErr.name, message: zipErr.message, stack: zipErr.stack }) + }) + } + /** * Builds a `handle` method for `ipcMain` to be called * with `invoke` method in `ipcRenderer`. @@ -59,15 +156,16 @@ export default class UnzipResourceHandler extends Handler { * ``` */ build () { - super.build(async (_: Event) =>{ + super.build(async (options: { will?: any }) => { try { // Only unzip if is a selfcustody version let version = this.storage.get('version') as string; + const device = this.storage.get('device') as string; + const resources = this.storage.get('resources') as string; + const os = this.storage.get('os') as string; + const isMac10 = this.storage.get('isMac10') as boolean; + if (version.match(/selfcustody.*/g)) { - const device = this.storage.get('device') as string; - const resources = this.storage.get('resources') as string; - const os = this.storage.get('os') as string; - const isMac10 = this.storage.get('isMac10') as boolean; version = version.split('tag/')[1]; const zipFilePath = join(resources, version, `krux-${version}.zip`) @@ -81,104 +179,22 @@ export default class UnzipResourceHandler extends Handler { this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack}) } } - - this.send(`${this.name}:data`, `Extracting ${zipFilePath}

`) - - const zipfile = await openZipFile(zipFilePath) - zipfile.readEntry() - - // Each fileName should be added to entries array - // that will be returned to client application - // This event should extract each file to - // a destination folder defined in store - zipfile.on('entry', async (entry) => { - - // Directory file names end with '/'. - // Note that entries for directories themselves are optional. - // An entry's fileName implicitly requires its parent directories to exist. - const destination = join(resources, entry.fileName) - - if (/\/$/.test(entry.fileName)) { - const onlyRootKruxFolder = /^(.*\/)?krux-v[0-9\.]+\/$/ - const deviceKruxFolder = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/$`) - if (onlyRootKruxFolder.test(entry.fileName) || deviceKruxFolder.test(entry.fileName)) { - this.send(`${this.name}:data`, `Creating ${destination}

`) - await mkdirAsync(destination) - } - zipfile.readEntry(); - } else { - - let ktoolKrux: RegExp; - let deviceKruxFirmwareBin: RegExp; - let deviceKruxFirmwareBinSig: RegExp; - let deviceKruxKboot: RegExp; - - if (os === 'linux') { - ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-linux$/ - } else if (os === 'darwin' && !isMac10) { - ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-mac$/ - } else if (os === 'darwin' && isMac10) { - ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-mac-10$/ - } else if (os === 'win32') { - ktoolKrux = /^(.*\\)?krux-v[0-9\.]+\\ktool-win\.exe$/ - } - - if (os === 'linux' || os === 'darwin') { - deviceKruxFirmwareBin = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/firmware.bin$`) - deviceKruxFirmwareBinSig = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/firmware.bin.sig$`) - deviceKruxKboot = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/kboot.kfpkg$`) - } else if (os === 'win32') { - deviceKruxFirmwareBin = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\firmware.bin$`) - deviceKruxFirmwareBinSig = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\firmware.bin.sig$`) - deviceKruxKboot = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\kboot.kfpkg$`) - } - - // (only extract device related files) - if ( - deviceKruxFirmwareBin.test(destination) || - deviceKruxFirmwareBinSig.test(destination) || - deviceKruxKboot.test(destination) || - ktoolKrux.test(destination) - ) { - - // create the destination file - const writeStream = createWriteStream(destination) - - this.send(`${this.name}:data`, `Extracting ${entry.fileName}...

`) - - // extract it - zipfile.openReadStream(entry, (entryError, readStream) => { - if (entryError) { - this.send(`${this.name}:error`, { name: entryError.name, message: entryError.message, stack: entryError.stack }) - } else { - readStream.on('end', () => { - this.send(`${this.name}:data`, `Extracted to ${destination}

`) - zipfile.readEntry() - }) - - readStream.on('error', (streamErr) => { - this.send(`${this.name}:error`, { name: streamErr.name, message: streamErr.message, stack: streamErr.stack }) - }) - - readStream.pipe(writeStream) - } - }) + this.onUnzip(zipFilePath, resources, device, os, isMac10, options) + } else if (version === 'Select version') { + const globfiles = await glob(`${resources}/**/@(krux-v*.zip|ktool-*)`) + if (globfiles.length > 0) { + if (globfiles[0].includes('.zip')) { + const zipFilePath = globfiles[0] + this.onUnzip(zipFilePath, resources, device, os, isMac10, options) } else { - zipfile.readEntry() + this.send(`${this.name}:success`, { will: options.will }) } + } else { + const error = new Error("No ktool found") + this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack}) } - }) - - zipfile.on('end', () => { - zipfile.close() - this.send(`${this.name}:success`, null) - }) - - zipfile.on('error', (zipErr) => { - this.send(`${this.name}:error`, { name: zipErr.name, message: zipErr.message, stack: zipErr.stack }) - }) } else { - this.send(`${this.name}:success`, null) + this.send(`${this.name}:success`, { will: options.will }) } } catch (error) { this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack }) diff --git a/lib/wipe.ts b/lib/wipe.ts new file mode 100644 index 00000000..88f1baf3 --- /dev/null +++ b/lib/wipe.ts @@ -0,0 +1,222 @@ +/// + +import { spawn } from 'child_process' +import { join } from 'path' +import { SudoerLinux, SudoerDarwin } from '@o/electron-sudo/src/sudoer' +import ElectronStore from 'electron-store' +import Handler from './handler' +import { SerialPort } from 'serialport' +import { glob } from 'glob' + +export default class FlashHandler extends Handler { + + constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { + super('krux:wipe', win, storage, ipcMain); + } + + /** + * Builds a `handle` method for `ipcMain` to be called + * with `invoke` method in `ipcRenderer`. + * + * @example + * ``` + * // change some key in store + * // some keys are forbidden to change + * // https://api.github.com/repos/selfcustody/krux/git/refs/tags + * methods: { + * async download () { + * await window.api.invoke('krux:store:set') + * + * window.api.onSuccess('krux:store:set', function(_, isChanged) { + * // ... do something + * }) + * + * window.api.onError('krux:store:set', function(_, error) { + * // ... do something + * }) + * } + * } + * + * ``` + */ + build () { + super.build(async (options) => { + // Store + const os = this.storage.get('os') as string + const isMac10 = this.storage.get('isMac10') as boolean + const resources = this.storage.get('resources') as string + const device = this.storage.get('device') as string + + // OS commands + const flash = { command: '', args: [] } + const chmod = { commands: [] } + + // set correct flash instructions + // if the device 'maixpy_dock' the board argument (-B) is 'dan', + // otherwise, is 'goE' + // SEE https://github.com/odudex/krux_binaries#flash-instructions + if (device.match(/maixpy_(m5stickv|amigo)/)) { + flash.args = ['--verbose', '-B', 'goE', '-b', '1500000'] + try { + const ports = await SerialPort.list() + + // m5stickv and amigo has two ports + // get the first + let found = false + ports.forEach((port) => { + if (port.productId == "0403" && !found) { + flash.args.push("-p") + flash.args.push(port.path) + found = true + } + }) + } catch (err) { + this.send(`${this.name}:error`, { was: 'wipe', done: false , name: err.name, message: err.message, stack: err.stack }) + } + } else if (device.match(/maixpy_(bit|cube)/)) { + flash.args = ['--verbose', '-B', 'goE', '-b', '1500000'] + try { + const ports = await SerialPort.list() + ports.forEach((port) => { + if (port.productId == "0403") { + flash.args.push("-p") + flash.args.push(port.path) + } + }) + } catch (err) { + this.send(`${this.name}:error`, { was: 'wipe', done: false , name: err.name, message: err.message, stack: err.stack }) + } + } else if (device.match(/maixpy_dock/g)) { + flash.args = ['--verbose', '-B', 'dan', '-b', '1500000'] + try { + const ports = await SerialPort.list() + ports.forEach((port) => { + if (port.productId == "7523") { + flash.args.push("-p") + flash.args.push(port.path) + } + }) + } catch (err) { + this.send(`${this.name}:error`, { was: 'wipe', done: false , name: err.name, message: err.message, stack: err.stack }) + } + } else if (device.match(/maixpy_yahboom/g)){ + flash.args = ['--verbose', '-B', 'goE', '-b', '1500000'] + try { + const ports = await SerialPort.list() + ports.forEach((port) => { + if (port.productId == "7523") { + flash.args.push("-p") + flash.args.push(port.path) + } + }) + } catch (error) { + this.send(`${this.name}:error`, { was: 'wipe', done: false, name: error.name, message: error.message, stack: error.stack }) + } + } else { + const error = new Error() + error.name = "Not Implemented Error" + error.message = `${device} isnt valid to flash` + this.send(`${this.name}:error`, { was: 'wipe', done: false, name: error.name, message: error.message, stack: error.stack }) + } + + flash.args.push('-E') + + // Choose the correct ktool flasher + let globfiles: string[] + if (os === 'linux') { + globfiles = await glob(`${resources}/**/ktool-linux`) + flash.command = globfiles[0] + chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) + } else if (os === 'win32') { + globfiles = await glob(`${resources}/**/ktool-win.exe`) + flash.command = globfiles[0] + } else if (os === 'darwin' && !isMac10) { + globfiles = await glob(`${resources}/**/ktool-mac`) + flash.command = globfiles[0] + chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) + } else if (os === 'darwin' && isMac10) { + globfiles = await glob(`${resources}/**/ktool-mac-10`) + flash.command = globfiles[0] + chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) + } + + // stack commands to be executed + const promises = chmod.commands.map((cmd) => { + return new Promise((resolve, reject) => { + let error = null + let buffer = Buffer.alloc(0) + + this.send(`${this.name}:data`, `\x1b[32m$> ${cmd.command} ${cmd.args.join(' ')}\x1b[0m\n\n`) + const script = spawn(cmd.command, cmd.args) + + script.stdout.on('data', (data) => { + buffer = Buffer.concat([buffer, data]) + this.send(`${this.name}:data`, buffer.toString()) + }) + + script.stderr.on('data', (data) => { + buffer = Buffer.concat([buffer, data]) + this.send(`${this.name}:data`, buffer.toString()) + error = true + }) + + script.on('close', (code) => { + if (error) { + error = new Error(buffer.toString()) + reject(error) + } + resolve() + }) + }) + }) + + await Promise.all(promises) + + // setup flash command + let flasher = null + + this.send(`${this.name}:data`, `\x1b[32m$> ${flash.command} ${flash.args.join(' ')}\x1b[0m\n\n`) + + if (os === 'linux') { + const sudoer = new SudoerLinux() + flasher = await sudoer.spawn(flash.command, flash.args.join(' '), { env: process.env }) + } else if (os === 'darwin') { + const sudoer = new SudoerDarwin() + flasher = await sudoer.spawn(flash.command, flash.args.join(' '), { env: process.env }) + } else if (os === 'win32') { + flasher = spawn(flash.command, flash.args) + } + + let err = null + let output = '' + + flasher.stdout.on('data', (data: any) => { + output = Buffer.from(data, 'utf-8').toString() + if (output.match(/\[ERROR\].*/g)) { + output = output.replace("\x1b[31m", "") + output = output.replace("\x1b[1m", "") + output = output.replace("\x1b[0m", "") + output = output.replace("\x1b[32m", "") + output = output.replace("\x1b[0m \n", "") + output = output.replace("[ERROR]", "") + err = new Error(output) + } + this.send(`${this.name}:data`, output) + }) + + flasher.stderr.on('data', (data: any) => { + output = Buffer.from(data, 'utf-8').toString() + err = new Error(output) + this.send(`${this.name}:data`, output) + }) + + flasher.on('close', (code: any) => { + if (err) { + this.send(`${this.name}:error`, { was: 'wipe', done: false , name: err.name, message: err.message, stack: err.stack }) + } else { + this.send(`${this.name}:success`, { was: 'wipe', done: true }) + } + }) + }) + } +} diff --git a/package.json b/package.json index ee1359fb..0af55eec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "krux-installer", - "version": "0.0.12", + "version": "0.0.13", "main": "dist-electron/main/index.js", "description": "Graphical User Interface to download, verify and flash Krux´s firmware on Kendryte K210 hardwares as bitcoin signature devices", "author": "qlrd <106913782+qlrd@users.noreply.github.com>", @@ -61,7 +61,6 @@ "chai": "^5.1.0", "electron": "^29.1.0", "electron-builder": "^24.4.0", - "glob": "^10.3.3", "markdownlint-cli": "^0.39.0", "mocha": "^10.2.0", "os-lang": "^3.1.1", @@ -83,6 +82,7 @@ "command-exists": "^1.2.9", "debug": "^4.3.4", "electron-store": "^8.1.0", + "glob": "^10.3.3", "serialport": "^12.0.0", "vite-plugin-vuetify": "^2.0.1", "vue-asciimorph": "^0.0.3", diff --git a/src/App.vue b/src/App.vue index eb020b9c..f5371a2f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -32,7 +32,7 @@ import DownloadTestFirmware from './pages/DownloadTestFirmware.vue'; import DownloadTestKboot from './pages/DownloadTestKboot.vue'; import DownloadTestKtool from './pages/DownloadTestKtool.vue'; import FlashToDevice from './pages/FlashToDevice.vue'; - +import WipeDevice from './pages/WipeDevice.vue'; /** * Methods: These function will * manipulate `page` and `data` variables @@ -53,7 +53,9 @@ import onKruxUnzip from './utils/onKruxUnzip'; import onKruxFlash from './utils/onKruxFlash'; import onKruxFlashData from './utils/onKruxFlashData'; import onKruxUnzipData from './utils/onKruxUnzipData'; - +import onKruxCheckIfItWillWipeHandler from './utils/onKruxCheckIfItWillWipeHandler'; +import onKruxWipe from './utils/onKruxWipe'; +import onKruxWipeData from './utils/onKruxWipeData'; /** * Reference for which component will be used as showing page */ @@ -94,7 +96,8 @@ const pages: Ref> = shallowRef({ 'DownloadTestFirmware': DownloadTestFirmware, 'DownloadTestKboot': DownloadTestKboot, 'DownloadTestKtool': DownloadTestKtool, - 'FlashToDevice': FlashToDevice + 'FlashToDevice': FlashToDevice, + 'WipeDevice': WipeDevice }) /** @@ -114,14 +117,17 @@ window.api.onSuccess('krux:store:set', onKruxStoreSet(data)); window.api.onSuccess('krux:verify:releases:fetch', onKruxVerifyReleasesFetch(data)); window.api.onSuccess('krux:check:resource', onKruxCheckResources(data)); window.api.onSuccess('krux:check:will:flash', onKruxCheckIfItWillFlashHandler(data)); +window.api.onSuccess('krux:check:will:wipe', onKruxCheckIfItWillWipeHandler(data)); window.api.onSuccess('krux:download:resources', onKruxDownloadResources(data)); window.api.onSuccess('krux:verify:releases:hash', onKruxVerifyReleasesHash(data)); window.api.onSuccess('krux:verify:releases:sign', onKruxVerifyReleaseSign(data)); window.api.onSuccess('krux:unzip', onKruxUnzip(data)); window.api.onSuccess('krux:flash', onKruxFlash(data)); +window.api.onSuccess('krux:wipe', onKruxWipe(data)); window.api.onData('krux:download:resources', onKruxDownloadResourcesData(data)); window.api.onData('krux:unzip', onKruxUnzipData(data)); window.api.onData('krux:flash', onKruxFlashData(data)); +window.api.onData('krux:wipe', onKruxWipeData(data)); window.api.onError('krux:change:page', onError(data)); window.api.onError('krux:store:get', onError(data)); window.api.onError('krux:store:set', onError(data)); @@ -130,6 +136,7 @@ window.api.onError('krux:verify:releases:fetch', onError(data)); window.api.onError('krux:check:resource',onError(data)); window.api.onError('krux:download:resources', onError(data)); window.api.onError('krux:flash', onError(data)); +window.api.onError('krux:wipe', onError(data)); window.api.onError('krux:unzip', onError(data)); /** diff --git a/src/pages/FlashToDevice.vue b/src/pages/FlashToDevice.vue index 92628894..419ae137 100644 --- a/src/pages/FlashToDevice.vue +++ b/src/pages/FlashToDevice.vue @@ -59,7 +59,6 @@ const allOutput: Ref = ref('') Methods */ async function backToFn () { - output.value = output.value.split(' ').splice(0).join('') await window.api.invoke('krux:store:get', { from: 'FlashToDevice', keys: ['device', 'version', 'os', 'isMac10', 'showFlash'] }) } @@ -68,8 +67,7 @@ async function exitAppFn () { } onMounted(async function () { - allOutput.value = allOutput.value.split(' ').splice(0).join('') - await window.api.invoke('krux:unzip') + await window.api.invoke('krux:unzip', { will: 'flash' }) }) watch(output, function(newValue) { diff --git a/src/pages/Main.vue b/src/pages/Main.vue index 414b95d6..240435af 100644 --- a/src/pages/Main.vue +++ b/src/pages/Main.vue @@ -44,6 +44,24 @@ + + + + + + {{ wipe }} + + + + + @@ -75,6 +93,7 @@ const props = defineProps<{ os: string, isMac10: boolean, showFlash: boolean, + showWipe: boolean, clickMessage: string }>() @@ -108,11 +127,28 @@ const flash = computed(() => { } }) +const wipe = computed(() => { + if (props.os === 'linux') { + return 'Wipe with ktool-linux' + } + else if (props.os === 'win32') { + return 'Wipe with ktool-win.exe' + } + else if (props.os === 'darwin' && !props.isMac10) { + return 'Wipe with ktool-mac' + } + else if (props.os === 'darwin' && props.isMac10) { + return 'Wipe with ktool-mac-10' + } + else { + return 'Wipe' + } +}) /** *Variables */ -const { showFlash } = toRefs(props) +const { showFlash, showWipe } = toRefs(props) /** @@ -130,7 +166,25 @@ async function flashDevice () { await window.api.invoke('krux:change:page', { page: 'FlashToDevice' }) } +async function wipeDevice () { + const message = [ + "\t\t\t\t\tWARNING: CRITICAL OPERATION", + "", + "You are about to initiate a FULL WIPE of this device. This operation will:", + "", + "- Permanently erase all saved data.", + "- Remove the existing firmware.", + "- Render the device non-functional until new firmware is re-flashed." + ].join("\n") + + const confirmed = confirm(message) + if (confirmed) { + await window.api.invoke('krux:change:page', { page: 'WipeDevice' }) + } +} + onMounted(async function () { + await window.api.invoke('krux:check:will:wipe') await window.api.invoke('krux:check:will:flash') }) - \ No newline at end of file + diff --git a/src/pages/WipeDevice.vue b/src/pages/WipeDevice.vue new file mode 100644 index 00000000..d492d03b --- /dev/null +++ b/src/pages/WipeDevice.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 4e81dad7..480c9704 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -15,6 +15,8 @@ async function add( function clean(data: Ref>): void { data.value.messages = [] data.value.indexes = [] + data.value.output = "" + data.value.done = false } async function close(data: Ref>): Promise { @@ -24,4 +26,4 @@ async function close(data: Ref>): Promise { } } -export default { add, clean, close } \ No newline at end of file +export default { add, clean, close } diff --git a/src/utils/onError.ts b/src/utils/onError.ts index d0a4c7b1..3077178b 100644 --- a/src/utils/onError.ts +++ b/src/utils/onError.ts @@ -11,6 +11,9 @@ export default function (data: Ref>): Function { ...result, backTo: 'Main' } + if (data.value.output) { + data.value.output = "" + } await window.api.invoke('krux:change:page', { page: 'ErrorMsg' }) } -} \ No newline at end of file +} diff --git a/src/utils/onKruxCheckIfItWillFlashHandler.ts b/src/utils/onKruxCheckIfItWillFlashHandler.ts index b34c36a1..b3f2c2d1 100644 --- a/src/utils/onKruxCheckIfItWillFlashHandler.ts +++ b/src/utils/onKruxCheckIfItWillFlashHandler.ts @@ -1,29 +1,40 @@ import { Ref } from "vue" +function wipeOrFlash(data: Ref>, kind: string): string { + let click = '' + if (data.value.os === 'linux') { + click = `${kind} with ktool-linux` + } + else if (data.value.os === 'win32') { + click = `${kind} with ktool-win.exe` + } + else if (data.value.os === 'darwin' && !data.value.isMac10) { + click = `${kind} with ktool-mac` + } + else if (data.value.os === 'darwin' && data.value.isMac10) { + click = `${kind} with ktool-mac-10` + } + return click +} + export default function (data: Ref>): Function { return function (_: Event, result: Record<'showFlash', boolean>): void { data.value.showFlash = result.showFlash if (!data.value.showFlash && data.value.device !== 'Select device') { if (data.value.version === 'Select version') { - data.value.clickMessage = `Please click 'Select version' to download sources` + if(data.value.showWipe) { + const click = wipeOrFlash(data, 'Wipe') + data.value.clickMessage = `Click 'Select version' or '${click}'` + } else { + data.value.clickMessage = `Click 'Select version'` + } } else { - data.value.clickMessage = `Please click 'Version: ${data.value.version}' to download sources for ${data.value.device}` + const click = wipeOrFlash(data, 'Wipe') + data.value.clickMessage = `Click 'Version: ${data.value.version}' or ${click} for ${data.value.device}` } } else { - let click = '' - if (data.value.os === 'linux') { - click = 'Flash with ktool-linux' - } - else if (data.value.os === 'win32') { - click = 'Flash with ktool-win.exe' - } - else if (data.value.os === 'darwin' && !data.value.isMac10) { - click = 'Flash with ktool-mac' - } - else if (data.value.os === 'darwin' && data.value.isMac10) { - click = 'Flash with ktool-mac-10' - } + const click = wipeOrFlash(data, 'Wipe/Flash') data.value.clickMessage = `Connect your ${data.value.device} device and power on it before click '${click}'` } } -} \ No newline at end of file +} diff --git a/src/utils/onKruxCheckIfItWillWipeHandler.ts b/src/utils/onKruxCheckIfItWillWipeHandler.ts new file mode 100644 index 00000000..fec6707d --- /dev/null +++ b/src/utils/onKruxCheckIfItWillWipeHandler.ts @@ -0,0 +1,24 @@ +import { Ref } from "vue" + +function wipeOrFlash(data: Ref>, kind: string): string { + let click = '' + if (data.value.os === 'linux') { + click = `${kind} with ktool-linux` + } + else if (data.value.os === 'win32') { + click = `${kind} with ktool-win.exe` + } + else if (data.value.os === 'darwin' && !data.value.isMac10) { + click = `${kind} with ktool-mac` + } + else if (data.value.os === 'darwin' && data.value.isMac10) { + click = `${kind} with ktool-mac-10` + } + return click +} + +export default function (data: Ref>): Function { + return function (_: Event, result: Record<'showWipe', boolean>): void { + data.value.showWipe = result.showWipe + } +} diff --git a/src/utils/onKruxFlash.ts b/src/utils/onKruxFlash.ts index 84956de0..d67c94fd 100644 --- a/src/utils/onKruxFlash.ts +++ b/src/utils/onKruxFlash.ts @@ -8,5 +8,6 @@ export default function ( result:Record ): void { data.value.done = result.done + data.value.output = "" } -} \ No newline at end of file +} diff --git a/src/utils/onKruxStoreGet.ts b/src/utils/onKruxStoreGet.ts index 87bd7652..65e29f26 100644 --- a/src/utils/onKruxStoreGet.ts +++ b/src/utils/onKruxStoreGet.ts @@ -292,11 +292,11 @@ export default function onKruxStoreGet (data: Ref>): Functio await setMainData(data, result) } - if (result.from === 'FlashToDevice') { + if (result.from === 'FlashToDevice' || result.from === 'WipeDevice') { messages.clean(data) await window.api.invoke('krux:change:page', { page: 'Main' }) setMainData(data, result) } } -} \ No newline at end of file +} diff --git a/src/utils/onKruxUnzip.ts b/src/utils/onKruxUnzip.ts index 5c7b96d6..7b0d586f 100644 --- a/src/utils/onKruxUnzip.ts +++ b/src/utils/onKruxUnzip.ts @@ -10,8 +10,16 @@ export default function ( ): Function { return async function ( _: Event, - result:string + result:Record<'will', string> ): Promise{ - await window.api.invoke('krux:flash') + console.log("DATA===============") + console.log(data) + console.log("RESULT===============") + console.log(result) + if (result.will == 'flash') { + await window.api.invoke('krux:flash') + } else if (result.will == 'wipe') { + await window.api.invoke('krux:wipe') + } } } diff --git a/src/utils/onKruxWipe.ts b/src/utils/onKruxWipe.ts new file mode 100644 index 00000000..d67c94fd --- /dev/null +++ b/src/utils/onKruxWipe.ts @@ -0,0 +1,13 @@ +import { Ref } from "vue"; + +export default function ( + data: Ref> +): Function { + return function ( + _: Event, + result:Record + ): void { + data.value.done = result.done + data.value.output = "" + } +} diff --git a/src/utils/onKruxWipeData.ts b/src/utils/onKruxWipeData.ts new file mode 100644 index 00000000..79df67a9 --- /dev/null +++ b/src/utils/onKruxWipeData.ts @@ -0,0 +1,27 @@ +import { Ref } from "vue"; + +// If you have no problems simply ignoring all type-checking features for this library, you have two options: +// Add @ts-ignore above all imports or Create a declaration file with any type, so all imports are automatically considered to be of any type. +// see https://stackoverflow.com/questions/56688893/how-to-use-a-module-when-it-could-not-find-a-declaration-file#answer-56690386 +// @ts-ignore +import { AnsiUp } from 'ansi_up' + +/** + * Stream shell output to web frontend + * @see https://www.appsloveworld.com/vuejs/100/8/stream-shell-output-to-web-front-end + * @param data + */ +export default function ( + data: Ref> +): Function { + return function ( + _: Event, + result:string + ): void { + const ansi = new AnsiUp() + let tmp = result.replace(/%\s/, "\n") + tmp = tmp.replace(/kiB\/s/g, "kiB/s\n") + + data.value.output = ansi.ansi_to_html(tmp).replace(/\n/gm, '
') + } +} diff --git a/test/e2e/specs/002.app-startup.spec.mts b/test/e2e/specs/002.app-startup.spec.mts index 24212c5b..de838cf4 100644 --- a/test/e2e/specs/002.app-startup.spec.mts +++ b/test/e2e/specs/002.app-startup.spec.mts @@ -22,7 +22,7 @@ describe('KruxInstaller start up', () => { const version = await browser.electron.execute(function (electron) { return electron.app.getVersion() }) - expect(version).to.be.equal('0.0.12') + expect(version).to.be.equal('0.0.13') }) }) diff --git a/test/e2e/specs/038-verified-official-release.spec.mts b/test/e2e/specs/038-verified-official-release.spec.mts index 1387635b..502a0b48 100644 --- a/test/e2e/specs/038-verified-official-release.spec.mts +++ b/test/e2e/specs/038-verified-official-release.spec.mts @@ -7,7 +7,7 @@ import { createRequire } from 'module' const App = createRequire(import.meta.url)('../pageobjects/app.page') -const SHA256 = "e9 b1 56 d4 d0 1e 80 17 ed 4f 2f ad ac 01 cb 07 fe b2 7e 8a 01 e3 c9 7e 01 9c f2 f9 03 86 e6 b2" +const SHA256 = "f2 54 69 2f 76 6d c6 b0 09 c8 ca 7f 43 b6 74 d0 88 06 26 85 bb 20 3b 85 0f 8b 70 2f 64 1b 59 35" describe('KruxInstaller VerifiedOfficialRelease page (show and click back button)', () => { let instance: any;