diff --git a/interface.d.ts b/interface.d.ts index f43c311ff..0d3ef898a 100644 --- a/interface.d.ts +++ b/interface.d.ts @@ -2,6 +2,7 @@ import fs from 'node:fs/promises' import path from 'path' import { dialog, shell } from 'electron' import kittycad from '@kittycad/lib' +import { MachinesListing } from 'lib/machineManager' export interface IElectronAPI { open: typeof dialog.showOpenDialog @@ -34,6 +35,8 @@ export interface IElectronAPI { } } kittycad + listMachines: () => Promise + getMachineApiIp: () => Promise } declare global { diff --git a/package.json b/package.json index 439fb2190..c7758c44e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@tweenjs/tween.js": "^23.1.1", "@xstate/inspect": "^0.8.0", "@xstate/react": "^3.2.2", + "bonjour-service": "^1.2.1", "codemirror": "^6.0.1", "decamelize": "^6.0.0", "electron-squirrel-startup": "^1.0.1", diff --git a/src/components/LowerRightControls.tsx b/src/components/LowerRightControls.tsx index 6db94b7e2..5d7bfd49f 100644 --- a/src/components/LowerRightControls.tsx +++ b/src/components/LowerRightControls.tsx @@ -92,7 +92,7 @@ export function LowerRightControls({ diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index d3a39c38d..b76f38177 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -23,23 +23,6 @@ const PROJECT_SETTINGS_FILE_NAME = 'project.toml' const PROJECT_FOLDER = 'zoo-modeling-app-projects' const DEFAULT_PROJECT_KCL_FILE = 'main.kcl' -// List machines on the local network. -export async function listMachines(): Promise<{ - [key: string]: components['schemas']['Machine'] -}> { - console.log("STUB") - return {} - // let machines: string = await invoke('list_machines') - // return JSON.parse(machines) -} - -// Get the machine-api ip address. -export async function getMachineApiIp(): Promise { - console.log("STUB") - return null - // return await invoke('get_machine_api_ip') -} - export async function renameProjectDirectory( projectPath: string, newName: string diff --git a/src/lib/electron.ts b/src/lib/electron.ts index 5e2c85e1c..1d8e9059d 100644 --- a/src/lib/electron.ts +++ b/src/lib/electron.ts @@ -2,6 +2,8 @@ import { ipcRenderer, contextBridge } from 'electron' import path from 'path' import fs from 'node:fs/promises' import packageJson from '../../package.json' +import { components } from 'lib/machine-api' +import { MachinesListing } from 'lib/machineManager' const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args) const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args) @@ -36,6 +38,18 @@ const exposeProcessEnv = (varName: string) => { } } +// We could probably do this from the renderer side, but I fear CORS will +// bite our butts. +const listMachines = async (): Promise => { + const machineApi = await ipcRenderer.invoke('find_machine_api') + if (!machineApi) return {} + + return fetch(`http://${machineApi}/machines`).then((resp) => resp.json()) +} + +const getMachineApiIp = async (): Promise => + ipcRenderer.invoke('find_machine_api') + import('@kittycad/lib').then((kittycad) => { contextBridge.exposeInMainWorld('electron', { login, @@ -70,5 +84,7 @@ import('@kittycad/lib').then((kittycad) => { kittycad: { users: kittycad.users, }, + listMachines, + getMachineApiIp, }) }) diff --git a/src/lib/machineManager.ts b/src/lib/machineManager.ts index 2b46eaa6f..fd9444e6a 100644 --- a/src/lib/machineManager.ts +++ b/src/lib/machineManager.ts @@ -1,12 +1,13 @@ import { isDesktop } from './isDesktop' import { components } from './machine-api' -import { getMachineApiIp, listMachines } from './desktop' + +export type MachinesListing = { + [key: string]: components['schemas']['Machine'] +} export class MachineManager { private _isDesktop: boolean = isDesktop() - private _machines: { - [key: string]: components['schemas']['Machine'] - } = {} + private _machines: MachinesListing = {} private _machineApiIp: string | null = null private _currentMachine: components['schemas']['Machine'] | null = null @@ -24,15 +25,21 @@ export class MachineManager { } // Start a background job to update the machines every ten seconds. - setInterval(() => { - this.updateMachineApiIp() - this.updateMachines() - }, 10000) + // If MDNS is already watching, this timeout will wait until it's done to trigger the + // finding again. + let timeoutId = undefined + const timeoutLoop = () => { + clearTimeout(timeoutId) + timeoutId = setTimeout(async () => { + await this.updateMachineApiIp() + await this.updateMachines() + timeoutLoop() + }, 10000) + } + timeoutLoop() } - get machines(): { - [key: string]: components['schemas']['Machine'] - } { + get machines(): MachinesListing { return this._machines } @@ -57,7 +64,7 @@ export class MachineManager { return } - this._machines = await listMachines() + this._machines = await window.electron.listMachines() console.log('Machines:', this._machines) } @@ -66,7 +73,7 @@ export class MachineManager { return } - this._machineApiIp = await getMachineApiIp() + this._machineApiIp = await window.electron.getMachineApiIp() } } diff --git a/src/lib/routeLoaders.ts b/src/lib/routeLoaders.ts index 2f13fe3f5..51027b77d 100644 --- a/src/lib/routeLoaders.ts +++ b/src/lib/routeLoaders.ts @@ -83,7 +83,8 @@ export const fileLoader: LoaderFunction = async ( const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH) if (!isBrowserProject && projectPathData) { - const { project_name, project_path, current_file_name, current_file_path } = projectPathData + const { project_name, project_path, current_file_name, current_file_path } = + projectPathData const urlObj = new URL(routerData.request.url) let code = '' diff --git a/src/main.ts b/src/main.ts index e9289f515..f95bce631 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' import path from 'path' import fs from 'node:fs/promises' import { Issuer } from 'openid-client' +import { Bonjour, Service } from 'bonjour-service' // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (require('electron-squirrel-startup')) { @@ -72,6 +73,10 @@ app.on('window-all-closed', () => { // Some APIs can only be used after this event occurs. app.on('ready', createWindow) +// For now there is no good reason to separate these out to another file(s) +// There is just not enough code to warrant it and further abstracts everything +// which is already quite abstracted + ipcMain.handle('app.getPath', (event, data) => { return app.getPath(data) }) @@ -137,3 +142,22 @@ ipcMain.handle('login', async (event, host) => { return tokenSet.access_token }) + +const SERVICE_NAME = '_machine-api._tcp.local.' + +ipcMain.handle('find_machine_api', () => { + const timeoutAfterMs = 5000 + return new Promise((resolve, reject) => { + // if it takes too long reject this promise + setTimeout(() => resolve(null), timeoutAfterMs) + const bonjourEt = new Bonjour({}, (error: Error) => { + console.log('An issue with Bonjour services was encountered!') + console.error(error) + resolve(null) + }) + console.log('Looking for machine API...') + bonjourEt.find({ type: SERVICE_NAME }, (service: Service) => { + resolve(service.fqdn) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 462b54236..bc8feff5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1981,6 +1981,11 @@ ts-node "^10.9.1" tslib "~2.4" +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + "@lezer/common@^1.0.0", "@lezer/common@^1.1.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049" @@ -3333,6 +3338,14 @@ body-parser@1.20.2: type-is "~1.6.18" unpipe "1.0.0" +bonjour-service@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + boolean@^3.0.1: version "3.2.0" resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" @@ -4052,6 +4065,13 @@ dlv@^1.1.3: resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -6596,6 +6616,14 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + mz@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -8452,6 +8480,11 @@ three@^0.166.1: resolved "https://registry.yarnpkg.com/three/-/three-0.166.1.tgz#322cfc48fff4e751cd47d61fd1558c387d098d7c" integrity sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg== +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + tiny-each-async@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/tiny-each-async/-/tiny-each-async-2.0.3.tgz#8ebbbfd6d6295f1370003fbb37162afe5a0a51d1"