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 00000000..28a10a3b Binary files /dev/null and b/test.db differ