Fix kcl file opening on Windows (double click) on second instance (#5420)

* WIP: Double-clicking on .kcl file on Windows redirects to the home page if the app is already open
Fixes #5412

* Add deep link test case for linux

* Add mac tests

* Lint and win tests

* Fix e2e tests

* Logs everywhere

* windows weird? yup

* More logzzz maybe it's not windows

* Remove :/// replacement. Add catch log

* Fix and clean up

* FIx lint

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* More lint

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Clean up tests further

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Pierre Jacquier
2025-02-20 15:23:39 -05:00
committed by GitHub
parent 9f5003cafc
commit 30029a63a1
5 changed files with 129 additions and 21 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,82 @@
import { getPathOrUrlFromArgs, parseCLIArgs } from 'commandLineArgs'
const linuxDeepLinkArgv = [
'/tmp/.mount_Zoo Movq3t0x/zoo-modeling-app',
'--no-sandbox',
'--allow-file-access-from-files',
'zoo-studio://?create-file=true&name=deeplinks&code=cGxhbmUwMDEgPSBvZmZzZXRQbGFuZSgnWFonLCBvZmZzZXQgPSA1KQ%3D%3D',
]
const linuxNoPathArgv = [
'/tmp/.mount_Zoo MogQS2hd/zoo-modeling-app',
'--no-sandbox',
'--allow-file-access-from-files',
]
const linuxPathArgv = [
'/tmp/.mount_Zoo MogQS2hd/zoo-modeling-app',
'--no-sandbox',
'--allow-file-access-from-files',
'/home/pierremtb/Documents/zoo-modeling-app-projects/project-001/main.kcl',
]
const winDeepLinkArgv = [
'C:\\Program Files\\Zoo Modeling App\\Zoo Modeling App.exe',
'--allow-file-access-from-files',
'zoo-studio:///?create-file=true&name=deeplinkscopy&code=cGxhbmUwMDEgPSBvZmZzZXRQbGFuZSgnWFonLCBvZmZzZXQgPSA1KQo%3D',
]
const winNoPathArgv = [
'C:\\Program Files\\Zoo Modeling App\\Zoo Modeling App.exe',
'--allow-file-access-from-files',
]
const winPathArgv = [
'C:\\Program Files\\Zoo Modeling App\\Zoo Modeling App.exe',
'--allow-file-access-from-files',
'C:\\Users\\pierr\\Documents\\zoo-modeling-app-projects\\deeplink\\main.kcl',
]
// macos doesn't uses the open-url scheme so is different so no macDeepLinkArgv
const macNoPathArgv = [
'/Applications/Zoo Modeling App.app/Contents/MacOS/Zoo Modeling App',
]
const macPathArgv = [
'/Applications/Zoo Modeling App.app/Contents/MacOS/Zoo Modeling App',
'/Users/pierremtb/Documents/zoo-modeling-app-projects/loft/main.kcl',
]
describe('getPathOrUrlFromArgs', () => {
;[
['linux', linuxDeepLinkArgv],
['windows', winDeepLinkArgv],
// macos doesn't uses the open-url scheme so is different
].map(([os, argv]) => {
it(`should parse second-instance deep link argv on ${os}`, () => {
const args = parseCLIArgs(argv as string[])
expect(getPathOrUrlFromArgs(args)).toContain('zoo-studio://')
})
})
;[
['linux', linuxPathArgv],
['windows', winPathArgv],
['mac', macPathArgv],
].map(([os, argv]) => {
it(`should parse path argv on ${os}`, () => {
const args = parseCLIArgs(argv as string[])
expect(getPathOrUrlFromArgs(args)).toContain('main.kcl')
})
})
;[
['linux', linuxNoPathArgv],
['windows', winNoPathArgv],
['mac', macNoPathArgv],
].map(([os, argv]) => {
it(`should return undefined without path argv on ${os}`, () => {
const args = parseCLIArgs(argv as string[])
expect(getPathOrUrlFromArgs(args)).toBeUndefined()
})
})
})

View File

@ -1,7 +1,8 @@
import minimist from 'minimist'
import yargs from 'yargs' import yargs from 'yargs'
import { hideBin } from 'yargs/helpers' import { hideBin } from 'yargs/helpers'
const argv = yargs(hideBin(process.argv)) export const argvFromYargs = yargs(hideBin(process.argv))
.option('telemetry', { .option('telemetry', {
alias: 't', alias: 't',
type: 'boolean', type: 'boolean',
@ -9,4 +10,20 @@ const argv = yargs(hideBin(process.argv))
}) })
.parse() .parse()
export default argv // TODO: find a better way to merge minimist and yargs parsers.
export function parseCLIArgs(argv: string[]): minimist.ParsedArgs {
return minimist(argv, {
// Treat all double-hyphenated arguments without equal signs as boolean
boolean: true,
})
}
export function getPathOrUrlFromArgs(
args: minimist.ParsedArgs
): string | undefined {
if (args._.length > 1) {
return args._[1]
}
return undefined
}

View File

@ -17,23 +17,27 @@ import { Bonjour, Service } from 'bonjour-service'
// @ts-ignore: TS1343 // @ts-ignore: TS1343
import * as kittycad from '@kittycad/lib/import' import * as kittycad from '@kittycad/lib/import'
import electronUpdater, { type AppUpdater } from 'electron-updater' import electronUpdater, { type AppUpdater } from 'electron-updater'
import minimist from 'minimist'
import getCurrentProjectFile from 'lib/getCurrentProjectFile' import getCurrentProjectFile from 'lib/getCurrentProjectFile'
import os from 'node:os' import os from 'node:os'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { ZOO_STUDIO_PROTOCOL } from 'lib/constants' import { ZOO_STUDIO_PROTOCOL } from 'lib/constants'
import argvFromYargs from './commandLineArgs' import {
argvFromYargs,
getPathOrUrlFromArgs,
parseCLIArgs,
} from './commandLineArgs'
import * as packageJSON from '../package.json' import * as packageJSON from '../package.json'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
// Check the command line arguments for a project path // Check the command line arguments for a project path
const args = parseCLIArgs() const args = parseCLIArgs(process.argv)
// @ts-ignore: TS1343 // @ts-ignore: TS1343
const viteEnv = import.meta.env const viteEnv = import.meta.env
const NODE_ENV = process.env.NODE_ENV || viteEnv.MODE const NODE_ENV = process.env.NODE_ENV || viteEnv.MODE
const IS_PLAYWRIGHT = process.env.IS_PLAYWRIGHT
// dotenv override when present // dotenv override when present
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] }) dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
@ -50,7 +54,8 @@ process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??=
viteEnv.VITE_KC_CONNECTION_TIMEOUT_MS viteEnv.VITE_KC_CONNECTION_TIMEOUT_MS
// Likely convenient to keep for debugging // Likely convenient to keep for debugging
console.log('process.env', process.env) console.log('Environment vars', process.env)
console.log('Parsed CLI args', args)
/// Register our application to handle all "zoo-studio:" protocols. /// Register our application to handle all "zoo-studio:" protocols.
const singleInstanceLock = app.requestSingleInstanceLock() const singleInstanceLock = app.requestSingleInstanceLock()
@ -68,7 +73,7 @@ if (process.defaultApp) {
// Must be done before ready event. // Must be done before ready event.
// Checking against this lock is needed for Windows and Linux, see // Checking against this lock is needed for Windows and Linux, see
// https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#windows-and-linux-code // https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#windows-and-linux-code
if (!singleInstanceLock && !process.env.IS_PLAYWRIGHT) { if (!singleInstanceLock && !IS_PLAYWRIGHT) {
app.quit() app.quit()
} else { } else {
registerStartupListeners() registerStartupListeners()
@ -100,12 +105,14 @@ const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
} }
// Deep Link: Case of a cold start from Windows or Linux // Deep Link: Case of a cold start from Windows or Linux
const zooProtocolArg = process.argv.find((a) => const pathOrUrl = getPathOrUrlFromArgs(args)
a.startsWith(ZOO_STUDIO_PROTOCOL + '://') if (
) !pathToOpen &&
if (!pathToOpen && zooProtocolArg) { pathOrUrl &&
pathToOpen = zooProtocolArg pathOrUrl.startsWith(ZOO_STUDIO_PROTOCOL + '://')
console.log('Retrieved deep link from argv', pathToOpen) ) {
pathToOpen = pathOrUrl
console.log('Retrieved deep link from CLI args', pathToOpen)
} }
// Deep Link: Case of a second window opened for macOS // Deep Link: Case of a second window opened for macOS
@ -431,7 +438,8 @@ const getProjectPathAtStartup = async (
// If we are in development mode, we don't want to load a project at // If we are in development mode, we don't want to load a project at
// startup. // startup.
// Since the args passed are always '.' // Since the args passed are always '.'
if (NODE_ENV !== 'production') { // aka Forge for yarn tron:start live dev or playwright tests, but not dev packaged apps
if (MAIN_WINDOW_VITE_DEV_SERVER_URL || IS_PLAYWRIGHT) {
return null return null
} }
@ -484,17 +492,18 @@ const getProjectPathAtStartup = async (
return null return null
} }
function parseCLIArgs(): minimist.ParsedArgs {
return minimist(process.argv, {})
}
function registerStartupListeners() { function registerStartupListeners() {
// Linux and Windows from https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app // Linux and Windows from https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app
app.on('second-instance', (event, commandLine, workingDirectory) => { app.on('second-instance', (event, commandLine, workingDirectory) => {
// Deep Link: second instance for Windows and Linux // Deep Link: second instance for Windows and Linux
const url = commandLine.pop()?.slice(0, -1) // Likely convenient to keep for debugging
console.log('Retrieved deep link from commandLine', url) console.log(
createWindow(url) 'Parsed CLI args from second instance',
parseCLIArgs(commandLine)
)
const pathOrUrl = getPathOrUrlFromArgs(parseCLIArgs(commandLine))
console.log('Retrieved path or deep link from second-instance', pathOrUrl)
createWindow(pathOrUrl)
}) })
/** /**