From ab0b10f922cf7c889feacaaf6bbded38e093b7d3 Mon Sep 17 00:00:00 2001 From: Jinhwan Kim Date: Sat, 26 Aug 2023 12:48:30 +0900 Subject: [PATCH] 23.08 updates - forge.config.js: remove unused code - get-r-mac.sh: update with R 4.3.1 - README.md: update versions, fix typo, add some trouble shooting - src/: code optimized --- README.md | 78 ++++++----- forge.config.js | 20 +-- get-r-mac.sh | 2 +- src/loading.html | 72 +++++----- src/main.js | 332 ++++++++++++++++++++++------------------------- 5 files changed, 231 insertions(+), 273 deletions(-) diff --git a/README.md b/README.md index e5e8a61..c0c3325 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ For more info, see previous " }, - "dependencies": { + "dependencies": { "axios": "0.27.2", + "electron-squirrel-startup": "^1.0.0", "esm": "^3.2.25", - "execa": "^5.1.1", - "electron-squirrel-startup": "^1.0.0" + "execa": "^5.1.1" }, "devDependencies": { - "@babel/core": "^7.21.0", - "@babel/plugin-transform-async-to-generator": "^7.20.7", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "eslint": "^8.35.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "fs-extra": "^11.1.0", - "@electron-forge/cli": "^6.0.5", - "@electron-forge/maker-deb": "^6.0.5", - "@electron-forge/maker-rpm": "^6.0.5", - "@electron-forge/maker-squirrel": "^6.0.5", - "@electron-forge/maker-zip": "^6.0.5", - "electron": "23.1.3" - } + "@babel/core": "^7.22.11", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/preset-env": "^7.22.10", + "@babel/preset-react": "^7.22.5", + "@electron-forge/cli": "^6.4.1", + "@electron-forge/maker-dmg": "^6.4.1", + "@electron-forge/maker-zip": "^6.4.1", + "@electron-forge/plugin-auto-unpack-natives": "^6.4.1", + "electron": "^26.1.0", + "fs-extra": "^11.1.0" ``` -> 💡 I checked every dependencies manually and it's latest version based 2023.03 (except `axios`, `axios` version 1 doesn't work yet) +> 💡 This work with 2023.08, do not update dependencies (axios & esm & execa) ### Shiny @@ -130,25 +123,20 @@ All of the following steps can be run exclusively in the RStudio **Terminal** (r ## Build Shiny Electron App -> 💡 You need to run this steps **whenever** you want to update Electron. +> 💡 You need to run this steps **whenever** you want to update shiny application. 15. In the Rstudio **terminal**, run `Rscript add-cran-binary-pkgs.R` to get packages for R. > Note: Modules are updated frequently and as such are subject to changing version numbers. It is important to double-check that these dependencies are up-to-date by replacing their version numbers with any newer version numbers. You can accomplish this by manually searching the module names at -> 💡 We are using `"eslint-plugin-react-hooks": "^1.7.0"` because using the latest v2.4.0 throws a warning. -> -> 💡 Didn't checked in 2023, but still `^1.7.0` works, so don't change it unless it requires. +16. Run `npm install` to add new dependencies you listed in `package.json` to the **node_modules** folder. +17. Test to see if your app works by running `electron-forge start` -16. Replace the `"lint": "echo \"No linting configured\""` line in `package.json` with `"lint": "eslint src --color"` -17. Run `npm install` to add new dependencies you listed in `package.json` to the **node_modules** folder. -18. Test to see if your app works by running `electron-forge start` +> 💡 If application keep running (not start), Try restart R with `CMD + Shift + 0` / **Session -> Restart R** in Rstudio. then retry 17. -> 💡 If application keep running (not start), Try restart R with `CMD + Shift + 0` / **Session -> Restart R** in Rstudio. then retry 18. +18. If the app runs successfully, congratulations! Package and create the `.exe` on the command line with `electron-forge make`. Your app can be found in the **/out** folder. -19. If the app runs successfully, congratulations! Package and create the `.exe` on the command line with `electron-forge make`. Your app can be found in the **/out** folder. - -Final. Unzip the result `zip file` and run `.app` +Final. Unzip the result `zip file` and run `.app` ------------------------------------------------------------------------ @@ -156,9 +144,10 @@ Final. Unzip the result `zip file` and run `.app` - Raise an issue, please. -#### require() of ES Module +#### Error with require() of ES Module - Change `dependencies` (see [issue](/../../issues/2)) + ``` "dependencies": { "axios": "^0.27.2", @@ -167,3 +156,10 @@ Final. Unzip the result `zip file` and run `.app` } ``` +#### Add not-CRAN packages + +- manually copy library from your Local's R library to **r-mac/library**, you can check with `.libPaths()` in R console. + +#### electron-forge start work, but electron-forge make not work + +- It seems to be problem caused by permission, **run .app file via terminal** with `sudo open ~~.app`. \ No newline at end of file diff --git a/forge.config.js b/forge.config.js index 9a4060f..4e99b1a 100644 --- a/forge.config.js +++ b/forge.config.js @@ -2,21 +2,13 @@ module.exports = { packagerConfig: {}, rebuildConfig: {}, makers: [ - { - name: '@electron-forge/maker-squirrel', - config: {}, - }, { name: '@electron-forge/maker-zip', - platforms: ['darwin'], - }, - { - name: '@electron-forge/maker-deb', - config: {}, - }, - { - name: '@electron-forge/maker-rpm', - config: {}, + platforms: ['darwin'] }, - ], + //{ + // name: '@electron-forge/maker-dmg', + // platforms: ['darwin'] + //} + ] }; diff --git a/get-r-mac.sh b/get-r-mac.sh index 87d5e2c..c31c5cf 100644 --- a/get-r-mac.sh +++ b/get-r-mac.sh @@ -8,7 +8,7 @@ set -e # Updated as big-sur / m1 mkdir -p r-mac curl -o r-mac/latest_r.pkg \ -https://cloud.r-project.org/bin/macosx/big-sur-arm64/base/R-4.2.2-arm64.pkg +https://cloud.r-project.org/bin/macosx/big-sur-arm64/base/R-4.3.1-arm64.pkg cd r-mac xar -xf latest_r.pkg diff --git a/src/loading.html b/src/loading.html index ec9fb0b..5c17966 100644 --- a/src/loading.html +++ b/src/loading.html @@ -1,44 +1,40 @@ - + - + - - -
-
-
-
-
-
-
-
- -
- - - + + +
+
+
+
+
+
+
+
+ + + diff --git a/src/main.js b/src/main.js index e62e34e..e393406 100644 --- a/src/main.js +++ b/src/main.js @@ -41,212 +41,186 @@ if (require('electron-squirrel-startup')) { // eslint-disable-line global-requir // use the onErrorStartup callback to react to a critical failure during startup // use the onErrorLater callback to handle the case when the R process dies // use onSuccess to retrieve the shinyUrl -const tryStartWebserver = async (attempt, progressCallback, onErrorStartup, - onErrorLater, onSuccess) => { - if (attempt > 3) { - await progressCallback({attempt: attempt, code: 'failed'}) - await onErrorStartup() - return - } - - if (rShinyProcess !== null) { - await onErrorStartup() // should not happen - return - } - - let shinyPort = randomPort() - - await progressCallback({attempt: attempt, code: 'start'}) - - let shinyRunning = false - const onError = async (e) => { - console.error(e) - rShinyProcess = null - if (shutdown) { // global state :( - return - } - if (shinyRunning) { - await onErrorLater() - } else { - await tryStartWebserver(attempt + 1, progressCallback, onErrorStartup, onErrorLater, onSuccess) - } - } - - let shinyProcessAlreadyDead = false - rShinyProcess = execa(rscript, - ['--vanilla', '-f', path.join(app.getAppPath(), 'start-shiny.R')], - { env: { - 'WITHIN_ELECTRON': '1', // can be used within an app to implement specific behaviour - 'RHOME': rpath, - 'R_HOME_DIR': rpath, - 'RE_SHINY_PORT': shinyPort, - 'RE_SHINY_PATH': shinyAppPath, - 'R_LIBS': libPath, - 'R_LIBS_USER': libPath, - 'R_LIBS_SITE': libPath, - 'R_LIB_PATHS': libPath} }).catch((e) => { - shinyProcessAlreadyDead = true - onError(e) - }) - - let url = `http://127.0.0.1:${shinyPort}` - for (let i = 0; i <= 10; i++) { - if (shinyProcessAlreadyDead) { - break - } - await waitFor(500) - try { - const res = await http.head(url, {timeout: 1000}) - // TODO: check that it is really shiny and not some other webserver - if (res.status === 200) { - await progressCallback({attempt: attempt, code: 'success'}) - shinyRunning = true - onSuccess(url) - return - } - } catch (e) { - - } - } - await progressCallback({attempt: attempt, code: 'notresponding'}) - - try { - rShinyProcess.kill() - } catch (e) {} - } - -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the JavaScript object is garbage collected. +const tryStartWebserver = async (attempt, progressCallback, onErrorStartup, onErrorLater, onSuccess) => { + if (attempt > 100) { + await progressCallback({attempt: attempt, code: 'failed'}) + await onErrorStartup() + return + } + + if (rShinyProcess !== null) { + await onErrorStartup() // should not happen + return + } + + let shinyPort = randomPort() + + await progressCallback({attempt: attempt, code: 'start'}) + + let shinyRunning = false + const onError = async (e) => { + console.error(e) + rShinyProcess = null + if (shutdown) { // global state :( + return + } + if (shinyRunning) { + await onErrorLater() + } else { + await tryStartWebserver(attempt + 1, progressCallback, onErrorStartup, onErrorLater, onSuccess) + } + } + + let shinyProcessAlreadyDead = false + rShinyProcess = execa(rscript, + ['--vanilla', '-f', path.join(app.getAppPath(), 'start-shiny.R')], + { env: { + 'WITHIN_ELECTRON': '1', // can be used within an app to implement specific behaviour + 'RHOME': rpath, + 'R_HOME_DIR': rpath, + 'RE_SHINY_PORT': shinyPort, + 'RE_SHINY_PATH': shinyAppPath, + 'R_LIBS': libPath, + 'R_LIBS_USER': libPath, + 'R_LIBS_SITE': libPath, + 'R_LIB_PATHS': libPath} }).catch((e) => { + shinyProcessAlreadyDead = true + onError(e) + }) + + let url = `http://127.0.0.1:${shinyPort}` + for (let i = 0; i <= 50; i++) { + if (shinyProcessAlreadyDead) { break } + await waitFor(500) + try { + const res = await http.head(url, {timeout: 1000}) + // TODO: check that it is really shiny and not some other webserver + if (res.status === 200) { + await progressCallback({attempt: attempt, code: 'success'}) + shinyRunning = true + onSuccess(url) + return + } + } catch (e) { } + } + await progressCallback({attempt: attempt, code: 'notresponding'}) + + try { rShinyProcess.kill() } catch (e) {} +} + + // Keep a global reference of the window object, if you don't, the window will + // be closed automatically when the JavaScript object is garbage collected. let mainWindow let loadingSplashScreen let errorSplashScreen const createWindow = (shinyUrl) => { -mainWindow = new BrowserWindow({ -width: 1600, -height: 900, -show: false, -webPreferences: { -nodeIntegration: false, -contextIsolation: true -} -}) - -mainWindow.loadURL(shinyUrl) - -// mainWindow.webContents.openDevTools() - -mainWindow.on('closed', () => { -mainWindow = null -}) + mainWindow = new BrowserWindow({ + width: 1600, + height: 900, + show: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + } + }) + + mainWindow.loadURL(shinyUrl) + + // mainWindow.webContents.openDevTools() + + mainWindow.on('closed', () => { + mainWindow = null + }) } const splashScreenOptions = { -width: 1600, -height: 900, -backgroundColor: backgroundColor + width: 1600, + height: 900, + backgroundColor: backgroundColor } const createSplashScreen = (filename) => { -const splashScreen = new BrowserWindow(splashScreenOptions) -splashScreen.loadURL(`file://${__dirname}/${filename}.html`) -splashScreen.on('closed', () => { -splashScreen = null -}) -return splashScreen + const splashScreen = new BrowserWindow(splashScreenOptions); + splashScreen.loadURL(`file://${__dirname}/${filename}.html`) + splashScreen.on('closed', () => { splashScreen = null }) + return splashScreen } const createLoadingSplashScreen = () => { -loadingSplashScreen = createSplashScreen('loading') + loadingSplashScreen = createSplashScreen('loading') } const createErrorScreen = () => { -errorSplashScreen = createSplashScreen('failed') + errorSplashScreen = createSplashScreen('failed') } // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', async () => { -// Set a content security policy -session.defaultSession.webRequest.onHeadersReceived((_, callback) => { -callback({ -responseHeaders: ` -default-src 'none'; -script-src 'self'; -img-src 'self' data:; -style-src 'self'; -font-src 'self'; -`}) -}) - -// Deny all permission requests -session.defaultSession.setPermissionRequestHandler((_1, _2, callback) => { -callback(false) + // Set a content security policy + session.defaultSession.webRequest.onHeadersReceived((_, callback) => { + callback({ + responseHeaders: ` + default-src 'none'; + script-src 'self'; + img-src 'self' data:; + style-src 'self'; + font-src 'self'; + `}) + }) + + // Deny all permission requests + session.defaultSession.setPermissionRequestHandler((_1, _2, callback) => { + callback(false) + }) + + createLoadingSplashScreen() + + const emitSpashEvent = async (event, data) => { + try { + await loadingSplashScreen.webContents.send(event, data) + } catch (e) {} + } + + // pass the loading events down to the loadingSplashScreen window + const progressCallback = async (event) => { + await emitSpashEvent('start-webserver-event', event) + } + + const onErrorLater = async () => { + if (!mainWindow) { return } + createErrorScreen() + await errorSplashScreen.show() + mainWindow.destroy() + } + + const onErrorStartup = async () => { + await waitFor(1000) // TODO: hack, only emit if the loading screen is ready + await emitSpashEvent('failed') + } + + try { + await tryStartWebserver(0, progressCallback, onErrorStartup, onErrorLater, (url) => { + createWindow(url) + loadingSplashScreen.destroy() + loadingSplashScreen = null + mainWindow.show() + }) + } catch (e) { + await emitSpashEvent('failed') + } }) -createLoadingSplashScreen() - -const emitSpashEvent = async (event, data) => { -try { -await loadingSplashScreen.webContents.send(event, data) -} catch (e) {} -} - -// pass the loading events down to the loadingSplashScreen window -const progressCallback = async (event) => { -await emitSpashEvent('start-webserver-event', event) -} - -const onErrorLater = async () => { -if (!mainWindow) { // fired when we quit the app -return -} -createErrorScreen() -await errorSplashScreen.show() -mainWindow.destroy() -} - -const onErrorStartup = async () => { -await waitFor(1000) // TODO: hack, only emit if the loading screen is ready -await emitSpashEvent('failed') -} - -try { -await tryStartWebserver(0, progressCallback, onErrorStartup, onErrorLater, (url) => { -createWindow(url) -loadingSplashScreen.destroy() -loadingSplashScreen = null -mainWindow.show() -}) -} catch (e) { -await emitSpashEvent('failed') -} -}) - -// Quit when all windows are closed. app.on('window-all-closed', () => { -// On OS X it is common for applications and their menu bar -// to stay active until the user quits explicitly with Cmd + Q -// if (process.platform !== 'darwin') { -// } -// We overwrite the behaviour for now as it makes things easier -// remove all events -shutdown = true -app.quit() - -// kill the process, just in case -// usually happens automatically if the main process is killed -try { -rShinyProcess.kill() -} catch (e) {} + shutdown = true + app.quit() + try { + rShinyProcess.kill() + } catch (e) {} }) app.on('activate', () => { -// On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - //if (mainWindow === null) { - // createWindow() - //} - // Deactivated for now + })