diff --git a/.github/workflows/Build-and-deploy-win.yml b/.github/workflows/Build-and-deploy-win.yml index 39795ae6b..ddbfe3f3d 100644 --- a/.github/workflows/Build-and-deploy-win.yml +++ b/.github/workflows/Build-and-deploy-win.yml @@ -7,6 +7,7 @@ on: - main - staging - pre-staging + - detect-firewall jobs: deploy-on-windows: diff --git a/src/main/index.js b/src/main/index.js index 48897a6cc..95a8d42c3 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -43,6 +43,11 @@ log.initialize({ preload: true }); log.transports.console.level = false; log.transports.file.level = "debug"; let user_restart_confirmed = false; +global.serverLive = true; + +ipcMain.handle("get-server-live-status", () => { + return global.serverLive; +}); let nodeStorage = new JSONStorage(app.getPath("userData")); @@ -331,12 +336,27 @@ const createPyProc = async () => { pyflaskProcess.stderr.on("data", (data) => { const logOutput = `[pyflaskProcess stderr] ${data.toString()}`; sessionServerOutput += `${logOutput}`; + global.serverLive = false; }); // On close, log the outputs and the exit code pyflaskProcess.on("close", (code) => { log.info(`child process exited with code ${code}`); log.info("Server output during session found below:"); log.info(sessionServerOutput); + global.serverLive = false; + }); + // Event listener for when the process exits + pyflaskProcess.on("exit", (code, signal) => { + if (signal) { + log.info(`Process was killed by signal: ${signal}`); + global.serverLive = false; + } else if (code !== 0) { + log.info(`Process exited with error code: ${code}`); + global.serverLive = false; + } else { + log.info("Process exited successfully"); + global.serverLive = false; + } }); } else { log.info("Application is not packaged"); @@ -351,10 +371,29 @@ const createPyProc = async () => { pyflaskProcess.on("error", function (err) { console.error("Failed to start pyflaskProcess:", err); + global.serverLive = false; }); pyflaskProcess.on("close", function (err) { console.error("Failed to start pyflaskProcess:", err); + global.serverLive = false; + }); + + // Event listener for when the process exits + pyflaskProcess.on("exit", (code, signal) => { + if (signal) { + global.serverLive = false; + + log.info(`Process was killed by signal: ${signal}`); + } else if (code !== 0) { + global.serverLive = false; + + log.info(`Process exited with error code: ${code}`); + } else { + global.serverLive = false; + + log.info("Process exited successfully"); + } }); } if (pyflaskProcess != null) { diff --git a/src/preload/index.js b/src/preload/index.js index b9988f3d5..69d0d6871 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -1,4 +1,4 @@ -import { contextBridge } from "electron"; +import { contextBridge, ipcRenderer } from "electron"; import { electronAPI } from "@electron-toolkit/preload"; import os from "os"; import fs from "fs-extra"; @@ -406,6 +406,12 @@ if (process.contextIsolated) { }); }, }); + contextBridge.exposeInMainWorld("server", { + serverIsLive: async () => { + const status = await ipcRenderer.invoke("get-server-live-status"); + return status; + }, + }); } catch (error) { log.error(error); } diff --git a/src/renderer/src/assets/css/buttons.css b/src/renderer/src/assets/css/buttons.css index 9307456c1..f6b758cee 100644 --- a/src/renderer/src/assets/css/buttons.css +++ b/src/renderer/src/assets/css/buttons.css @@ -505,3 +505,11 @@ #copy-icon-client-id:hover { cursor: pointer; } + +#copy-icon-firewall-docs { + margin-left: 5px; +} + +#copy-icon-firewall-docs:hover { + cursor: pointer; +} diff --git a/src/renderer/src/main.js b/src/renderer/src/main.js index 887a07bbb..0f19ce534 100644 --- a/src/renderer/src/main.js +++ b/src/renderer/src/main.js @@ -2,7 +2,7 @@ import "./assets/imports"; // Render React Components into their respective slots in the DOM import "./components/renderers/ReactComponentRenderer"; - +import "./scripts/check-firewall/checkFirewall"; import "./assets/demo-btns"; import "./assets/nav"; import "./scripts/client"; diff --git a/src/renderer/src/scripts/check-firewall/checkFirewall.js b/src/renderer/src/scripts/check-firewall/checkFirewall.js new file mode 100644 index 000000000..9e16436a5 --- /dev/null +++ b/src/renderer/src/scripts/check-firewall/checkFirewall.js @@ -0,0 +1,50 @@ +import axios from "axios"; +import { clientError } from "../others/http-error-handler/error-handler"; + +/** + * This function checks if the client is blocked by an external firewall. + * Assumptions: The client is connected to the internet. + * Returns: true if the client is blocked by an external firewall, false otherwise. + */ +export const clientBlockedByExternalFirewall = async (url) => { + // check that the client can make an api request to Pennsieve's public API + //make an axios request to this public endpoint: https://api.pennsieve.io/discover/datasets + //if the request fails, the client is blocked by an external firewall + try { + await axios.get(url); + return false; + } catch (error) { + clientError(error); + if (!error.response) { + // the request was made but no response was received. May be a firewall issue or the client + // may just need to wait to try again later. + return true; + } + + // there is not a firewall issue if we get an actual repsonse from the server + return false; + } +}; + +let docsUrl = "https://docs.sodaforsparc.io/how-to/how-to-resolve-network-issues"; +const copyClientIdToClipboard = () => { + window.electron.ipcRenderer.invoke("clipboard-write", docsUrl, "clipboard"); +}; + +const commonHTML = `

Please refer to the SODA documentation page on resolving this issue by either clicking here or by copying the url to the documentation page with the copy icon below.

+ +
+

${docsUrl}

+
+
`; + +export const blockedMessage = ` +

SODA is unable to reach Pennsieve. + If this issue persists it is possible that your network is blocking access to Pennsieve from SODA. +

+ ${commonHTML}`; + +export const hostFirewallMessage = `

SODA is unable to communicate with its server. + If this issue persists it is possible that your network is blocking access. +

+ ${commonHTML}`; diff --git a/src/renderer/src/scripts/globals.js b/src/renderer/src/scripts/globals.js index 28a063429..ab1cd09db 100644 --- a/src/renderer/src/scripts/globals.js +++ b/src/renderer/src/scripts/globals.js @@ -1,14 +1,13 @@ import Swal from "sweetalert2"; import "bootstrap-select"; import DragSort from "@yaireo/dragsort"; - import api from "./others/api/api"; import { clientError, userErrorMessage } from "./others/http-error-handler/error-handler"; import client from "./client"; -import { swalShowError } from "./utils/swal-utils"; -// import { window.clearValidationResults } from './validator/validate' +import { swalShowError, swalShowInfo } from "./utils/swal-utils"; // // Purpose: Will become preload.js in the future. For now it is a place to put global variables/functions that are defined in javascript files // // needed by the renderer process in order to run. +import { clientBlockedByExternalFirewall, blockedMessage } from "./check-firewall/checkFirewall"; // // Contributors table for the dataset description editing page const currentConTable = document.getElementById("table-current-contributors"); @@ -907,6 +906,13 @@ window.resetFFMUI = (ev) => { }; window.addBfAccount = async (ev, verifyingOrganization = False) => { + // check for external firewall interference (aspirational in that may not be foolproof) + const pennsieveURL = "https://api.pennsieve.io/discover/datasets"; + const blocked = await clientBlockedByExternalFirewall(pennsieveURL); + if (blocked) { + await swalShowInfo("Potential Firewall Interference", blockedMessage); + return; + } let footerMessage = "No existing accounts to load. Please add an account."; if (window.bfAccountOptionsStatus === "") { if (Object.keys(bfAccountOptions).length === 1) { @@ -1346,6 +1352,8 @@ var dropdownEventID = ""; window.openDropdownPrompt = async (ev, dropdown, show_timer = true) => { // if users edit current account if (dropdown === "bf") { + console.log("Calling opendropdown here?"); + await window.addBfAccount(ev, false); } else if (dropdown === "dataset") { dropdownEventID = ev?.id ?? ""; diff --git a/src/renderer/src/scripts/others/renderer.js b/src/renderer/src/scripts/others/renderer.js index 6b4992f25..0b4abe678 100644 --- a/src/renderer/src/scripts/others/renderer.js +++ b/src/renderer/src/scripts/others/renderer.js @@ -79,6 +79,11 @@ import { setPennsieveAgentCheckInProgress, setPostPennsieveAgentCheckAction, } from "../../stores/slices/backgroundServicesSlice"; +import { + clientBlockedByExternalFirewall, + blockedMessage, + hostFirewallMessage, +} from "../check-firewall/checkFirewall"; // add jquery to the window object window.$ = jQuery; @@ -462,6 +467,24 @@ const startupServerAndApiCheck = async () => { { value: 1 } ); + let serverIsLive = await window.server.serverIsLive(); + if (serverIsLive) { + // notify the user that there may be a firewall issue preventing the client from connecting to the server + Swal.close(); + await Swal.fire({ + icon: "info", + title: "Potential Network Issues", + html: hostFirewallMessage, + heightAuto: false, + backdrop: "rgba(0,0,0, 0.4)", + confirmButtonText: "Restart SODA To Try Again", + allowOutsideClick: false, + allowEscapeKey: false, + width: 800, + }); + await window.electron.ipcRenderer.invoke("relaunch-soda"); + } + Swal.close(); await Swal.fire({ icon: "error", @@ -479,6 +502,7 @@ const startupServerAndApiCheck = async () => { // Check app version on current app and display in the side bar // Also check the core systems to make sure they are all operational const initializeSODARenderer = async () => { + // TODO: Add check for internal firewall that blocks us from talking to the server here (detect-firewall) // check that the server is live and the api versions match // If this fails after the allotted time, the app will restart await startupServerAndApiCheck(); @@ -489,10 +513,11 @@ const initializeSODARenderer = async () => { //Refresh the Pennsieve account list if the user has connected their Pennsieve account in the past if (hasConnectedAccountWithPennsieve()) { - try { - // window.updateBfAccountList(); - } catch (error) { - clientError(error); + // check for external firewall interference (aspirational in that may not be foolproof) + const pennsieveURL = "https://api.pennsieve.io/discover/datasets"; + const blocked = await clientBlockedByExternalFirewall(pennsieveURL); + if (blocked) { + swalShowInfo("Potential Network Issue Detected", blockedMessage); } }