From 5d9991aa1a70f77d7e1e2f81367544e2b8a2020d Mon Sep 17 00:00:00 2001 From: "Leslie H." Date: Thu, 20 Jun 2024 11:49:39 -0500 Subject: [PATCH] Clean up code --- ext/app/electron/GristApp.ts | 49 ++++++++++++++++----------------- ext/app/electron/config.ts | 52 ++++++++++++----------------------- ext/app/electron/main.ts | 8 +----- test.db | Bin 0 -> 303104 bytes 4 files changed, 42 insertions(+), 67 deletions(-) create mode 100644 test.db diff --git a/ext/app/electron/GristApp.ts b/ext/app/electron/GristApp.ts index e3f86168..4c5780ea 100644 --- a/ext/app/electron/GristApp.ts +++ b/ext/app/electron/GristApp.ts @@ -5,9 +5,9 @@ import * as log from "app/server/lib/log"; import * as path from "path"; import * as shutdown from "app/server/lib/shutdown"; import * as winston from "winston"; +import { ElectronServerMethods, FlexServer } from "app/server/lib/FlexServer"; import { GristDesktopAuthMode, getMinimalElectronLoginSystem } from "app/electron/logins"; import AppMenu from "app/electron/AppMenu"; -import { FlexServer } from "app/server/lib/FlexServer"; import RecentItems from "app/common/RecentItems"; import { UpdateManager } from "app/electron/UpdateManager"; import { makeId } from "app/server/lib/idUtils"; @@ -18,13 +18,13 @@ import webviewOptions from "app/electron/webviewOptions"; export class GristApp { private flexServer: FlexServer; private app = electron.app; - private appWindows = new Set(); // A set of all our window objects. - private appHost: any = null; // The hostname to connect to the local node server we start. - private pendingPathToOpen: any = null; // Path to open when app is started to open a document. + private appWindows: Set = new Set(); // A set of all our window objects. + private appHost: string; // The hostname to connect to the local node server we start. + private pendingPathToOpen: string | undefined = undefined; // Path to open when app is started to open a document. // Function, set once the app is ready, that opens or focuses a window when Grist is started. It // is called on 'ready' and by onInstanceStart (triggered when starting another Grist instance). - private onStartup: any = null; + private onStartup: (optPath?: string) => Promise; private credential: string = makeId(); private shouldQuit = false; private authMode: GristDesktopAuthMode; @@ -74,36 +74,36 @@ export class GristApp { this.app.whenReady().then(() => this.onReady().catch(reportErrorAndStop)); } - private onInstanceStart(argv: any, workingDir: any) { + private onInstanceStart(argv: string[], workingDir: string) { argv = this.cleanArgv(argv); // Someone tried to run a second instance, we should either open a file or focus a window. log.debug("onInstanceStart %s in %s", JSON.stringify(argv), workingDir); if (this.onStartup) { - this.onStartup(argv[1] ? path.resolve(workingDir, argv[1]) : null); + this.onStartup(argv[1] ? path.resolve(workingDir, argv[1]) : undefined); } } - private cleanArgv(argv: any) { + private cleanArgv(argv: string[]) { // Ignoring flags starting with '-' which might be added by electron on Mac (See // https://phab.getgrist.com/T307). - return argv.filter((arg: any) => !arg.startsWith('-')); + return argv.filter((arg) => !arg.startsWith('-')); } - private openWindowForPath(path: string, openWith?: {loadURL: (url: string) => Promise}) { + private openWindowForDoc(docID: string, openWith?: {loadURL: (url: string) => Promise}) { // Create the browser window, and load the document. - (openWith || this.createWindow()).loadURL(this.getUrl({doc: path})); + (openWith || this.createWindow()).loadURL(this.getUrl(docID)); } // Opens file at filepath for any accepted file type. - private async handleOpen(serverMethods: any, filepath: string) { + private async handleOpen(serverMethods: ElectronServerMethods, filepath: string) { log.debug("handleOpen %s", filepath); const ext = path.extname(filepath); switch (ext) { case '.csv': case '.xlsx': case '.xlsm': { - const docName = serverMethods.importDoc(filepath); - this.openWindowForPath(docName); + const doc = await serverMethods.importDoc(filepath); + this.openWindowForDoc(doc.id); break; } default: @@ -112,12 +112,10 @@ export class GristApp { } } - private getUrl(options: { - doc?: string, - } = {}) { + private getUrl(docID?: string) { const url = new URL(this.appHost); - if (options.doc) { - url.pathname = 'doc/' + encodeURIComponent(options.doc); + if (docID) { + url.pathname = 'doc/' + encodeURIComponent(docID); } if (this.authMode !== 'none') { url.searchParams.set('electron_key', this.credential); @@ -171,7 +169,7 @@ export class GristApp { if (!await fse.pathExists(link)) { await fse.symlink(target, link, 'junction'); } - this.openWindowForPath(docId, openWith); + this.openWindowForDoc(docId, openWith); } // Returns the last Grist window that was created. @@ -261,7 +259,7 @@ export class GristApp { if (process.env.GRIST_LOG_PATH || fse.existsSync(debugLogPath)) { const output = fse.createWriteStream(debugLogPath, { flags: "a" }); - output.on('error', (err: any) => log.error("Failed to open %s: %s", debugLogPath, err)); + output.on('error', (err) => log.error("Failed to open %s: %s", debugLogPath, err)); output.on('open', () => { log.info('Logging also to %s', debugLogPath); output.write('\n--- log starting by pid ' + process.pid + ' ---\n'); @@ -284,7 +282,8 @@ export class GristApp { } private async onReady() { - this.appHost = process.env.APP_HOME_URL; + // APP_HOME_URL is set by loadConfig + this.appHost = process.env.APP_HOME_URL as string; await updateDb(); @@ -297,7 +296,7 @@ export class GristApp { // This function is what we'll call now, and also in onInstanceStart. The latter is used on // Windows thanks to makeSingleInstance, and triggered when user clicks another .grist file. // We can only set this callback once we have serverMethods and appHost. - this.onStartup = async (optPath: any) => { + this.onStartup = async (optPath?: string) => { log.debug("onStartup %s", optPath); if (optPath) { await this.handleOpen(serverMethods, optPath); @@ -305,7 +304,7 @@ export class GristApp { } const win = this.getLastWindow(); if (win) { - (win as any).show(); + win.show(); return; } // We had no file to open, so open a window to the DocList. @@ -314,7 +313,7 @@ export class GristApp { // Call onStartup immediately. this.onStartup(this.pendingPathToOpen); - this.pendingPathToOpen = null; + this.pendingPathToOpen = undefined; const recentItems = new RecentItems({ maxCount: 10, diff --git a/ext/app/electron/config.ts b/ext/app/electron/config.ts index fa5ff632..e1195457 100644 --- a/ext/app/electron/config.ts +++ b/ext/app/electron/config.ts @@ -1,36 +1,17 @@ +import * as dotenv from "dotenv"; import * as electron from "electron"; import * as fse from "fs-extra"; import * as log from "app/server/lib/log"; -import * as net from 'net'; import * as packageJson from "desktop.package.json"; import * as path from "path"; -import bluebird from 'bluebird'; import { commonUrls } from "app/common/gristUrls"; +import { getAvailablePort } from "app/server/lib/serverUtils"; -const NO_VALIDATION = () => true; +const NO_VALIDATION = () => true; -/** - * Copied from grist-core, since it is unsafe to import core code at this point. - */ -async function getAvailablePort(firstPort: number = 8000, optCount: number = 200): Promise { - const lastPort = firstPort + optCount - 1; - async function checkNext(port: number): Promise { - if (port > lastPort) { - throw new Error("No available ports between " + firstPort + " and " + lastPort); - } - return new bluebird((resolve: (p: number) => void, reject: (e: Error) => void) => { - const server = net.createServer(); - server.on('error', reject); - server.on('close', () => resolve(port)); - server.listen(port, 'localhost', () => server.close()); - }) - .catch(() => checkNext(port + 1)); - } - return bluebird.try(() => checkNext(firstPort)); -} -function check(envKey: string, validator: (value: string) => boolean, defaultValue: string,): void { +function validateOrFallback(envKey: string, validator: (value: string) => boolean, defaultValue: string,): void { const envValue = process.env[envKey]; if (envValue === undefined) { log.warn(`${envKey} is not set, using default value ${defaultValue}`); @@ -45,6 +26,7 @@ function check(envKey: string, validator: (value: string) => boolean, defaultVal export async function loadConfig() { + dotenv.config(); if (process.env.GRIST_ELECTRON_AUTH !== undefined) { if (process.env.GRIST_DESKTOP_AUTH === undefined) { process.env.GRIST_DESKTOP_AUTH = process.env.GRIST_ELECTRON_AUTH; @@ -53,22 +35,22 @@ export async function loadConfig() { log.warn("GRIST_DESKTOP_AUTH set, ignoring GRIST_ELECTRON_AUTH (deprecated)."); } } - check( + validateOrFallback( "GRIST_DEFAULT_USERNAME", NO_VALIDATION, "You" ); - check( + validateOrFallback( "GRIST_DEFAULT_EMAIL", NO_VALIDATION, "you@example.com" ); - check( + validateOrFallback( "GRIST_HOST", NO_VALIDATION, "localhost" ); - check( + validateOrFallback( "GRIST_PORT", (portstr) => { if (! /^\d+$/.test(portstr)) { @@ -79,43 +61,43 @@ export async function loadConfig() { }, (await getAvailablePort(47478)).toString() ); - check( + validateOrFallback( "GRIST_DESKTOP_AUTH", (auth) => ["strict", "none", "mixed"].includes(auth), "strict" ); - check( + validateOrFallback( "GRIST_SANDBOX_FLAVOR", (flavor) => ["pyodide", "unsandboxed", "gvisor", "macSandboxExec"].includes(flavor), "pyodide" ); - check( + validateOrFallback( "GRIST_INST_DIR", NO_VALIDATION, electron.app.getPath("userData") ); - check( + validateOrFallback( "GRIST_DATA_DIR", NO_VALIDATION, electron.app.getPath("documents") ); - check( + validateOrFallback( "GRIST_USER_ROOT", NO_VALIDATION, path.join(electron.app.getPath("home"), ".grist") ); - check( + validateOrFallback( "TYPEORM_DATABASE", NO_VALIDATION, path.join(electron.app.getPath("appData"), "landing.db") ); - check( + validateOrFallback( "GRIST_WIDGET_LIST_URL", // Related to plugins (Would have to be changed if local custom widgets are used?) NO_VALIDATION, commonUrls.gristLabsWidgetRepository ); - const homeDBLocation = path.parse((process.env.TYPEORM_DATABASE as string)).dir; + const homeDBLocation = path.parse(path.resolve(process.env.TYPEORM_DATABASE as string)).dir; if (!fse.existsSync(homeDBLocation)) { log.warn(`Directory to contain the home DB does not exist, creating ${homeDBLocation}`); fse.mkdirSync(homeDBLocation); diff --git a/ext/app/electron/main.ts b/ext/app/electron/main.ts index ed740fc3..63a5d5ea 100644 --- a/ext/app/electron/main.ts +++ b/ext/app/electron/main.ts @@ -1,4 +1,3 @@ -import * as dotenv from "dotenv"; import * as electron from "electron"; import * as path from "path"; import { program } from "commander"; @@ -18,6 +17,7 @@ if (!electron.app.isPackaged) { // eslint-disable-next-line sort-imports import * as packageJson from "desktop.package.json"; import * as version from "app/common/version"; +import { GristApp } from "app/electron/GristApp"; import { loadConfig } from "app/electron/config"; program.name(packageJson.name).version(`${packageJson.productName} ${packageJson.version} (with Grist Core ${version.version})`); @@ -30,12 +30,6 @@ if (!electron.app.isPackaged) { process.argv.splice(1, 1); } -dotenv.config(); - loadConfig().then(() => { - // Note: TYPEORM_DATABASE must be set before importing dbUtils, or else it won't take effect. - // As Grist code could pull dbUtils in implicitly, it is unsafe to import anything from Grist Core before this. - // eslint-disable-next-line @typescript-eslint/no-var-requires - const GristApp = require("app/electron/GristApp").GristApp; new GristApp().main(); }); diff --git a/test.db b/test.db new file mode 100644 index 0000000000000000000000000000000000000000..28a10a3b718b86b4c51cc20a0879b5915e6fd777 GIT binary patch literal 303104 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 literal 0 HcmV?d00001