204 lines
6.4 KiB
TypeScript
204 lines
6.4 KiB
TypeScript
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
import { ChildProcess, spawn } from 'child_process'
|
|
import electronPath, { MenuItem } from 'electron'
|
|
import path from 'path'
|
|
import { deleteDir, ensureDirectoryExists, readJSONFile } from '../app/javascripts/Main/Utils/fileUtils'
|
|
import { Language } from '../app/javascripts/Main/spellcheckerManager'
|
|
import { StoreKeys } from '../app/javascripts/Main/store'
|
|
import { UpdateState } from '../app/javascripts/Main/updateManager'
|
|
import { CommandLineArgs } from '../app/javascripts/Shared/CommandLineArgs'
|
|
import { AppMessageType, AppTestMessage, MessageType, TestIPCMessage, TestIPCMessageResult } from './TestIpcMessage'
|
|
|
|
function spawnAppprocess(userDataPath: string) {
|
|
const p = spawn(
|
|
electronPath as any,
|
|
[path.join(__dirname, '..'), CommandLineArgs.Testing, CommandLineArgs.UserDataPath, userDataPath],
|
|
{
|
|
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
},
|
|
)
|
|
return p
|
|
}
|
|
|
|
class Driver {
|
|
private appProcess: ChildProcess
|
|
|
|
private calls: Array<{
|
|
resolve: (...args: any) => void
|
|
reject: (...args: any) => void
|
|
} | null> = []
|
|
|
|
private awaitedOnMessages: Array<{
|
|
type: AppMessageType
|
|
resolve: (...args: any) => void
|
|
}> = []
|
|
|
|
appReady: Promise<unknown>
|
|
windowLoaded: Promise<unknown>
|
|
|
|
constructor(readonly userDataPath: string) {
|
|
this.appProcess = spawnAppprocess(userDataPath)
|
|
this.appProcess.on('message', this.receive)
|
|
this.appReady = this.waitOn(AppMessageType.Ready)
|
|
this.windowLoaded = this.waitOn(AppMessageType.WindowLoaded)
|
|
}
|
|
|
|
private receive = (message: TestIPCMessageResult | AppTestMessage) => {
|
|
if ('type' in message) {
|
|
if (message.type === AppMessageType.Log) {
|
|
console.log(message)
|
|
}
|
|
|
|
this.awaitedOnMessages = this.awaitedOnMessages.filter(({ type, resolve }) => {
|
|
if (type === message.type) {
|
|
resolve()
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
if ('id' in message) {
|
|
const call = this.calls[message.id]!
|
|
this.calls[message.id] = null
|
|
if (message.reject) {
|
|
call.reject(message.reject)
|
|
} else {
|
|
call.resolve(message.resolve)
|
|
}
|
|
}
|
|
}
|
|
|
|
private waitOn = (messageType: AppMessageType) => {
|
|
return new Promise((resolve) => {
|
|
this.awaitedOnMessages.push({
|
|
type: messageType,
|
|
resolve,
|
|
})
|
|
})
|
|
}
|
|
|
|
private send = (type: MessageType, ...args: any): Promise<any> => {
|
|
const id = this.calls.length
|
|
const message: TestIPCMessage = {
|
|
id,
|
|
type,
|
|
args,
|
|
}
|
|
|
|
this.appProcess.send(message)
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.calls.push({ resolve, reject })
|
|
})
|
|
}
|
|
|
|
windowCount = (): Promise<number> => this.send(MessageType.WindowCount)
|
|
|
|
appStateCall = (methodName: string, ...args: any): Promise<any> =>
|
|
this.send(MessageType.AppStateCall, methodName, ...args)
|
|
|
|
readonly window = {
|
|
signOut: (): Promise<void> => this.send(MessageType.SignOut),
|
|
}
|
|
|
|
readonly storage = {
|
|
dataOnDisk: async (): Promise<{ [key in StoreKeys]: any }> => {
|
|
const location = await this.send(MessageType.StoreSettingsLocation)
|
|
return readJSONFile(location)
|
|
},
|
|
dataLocation: (): Promise<string> => this.send(MessageType.StoreSettingsLocation),
|
|
setZoomFactor: (factor: number) => this.send(MessageType.StoreSet, 'zoomFactor', factor),
|
|
setLocalStorageValue: (key: string, value: string): Promise<void> =>
|
|
this.send(MessageType.SetLocalStorageValue, key, value),
|
|
}
|
|
|
|
readonly appMenu = {
|
|
items: (): Promise<MenuItem[]> => this.send(MessageType.AppMenuItems),
|
|
clickLanguage: (language: Language) => this.send(MessageType.ClickLanguage, language),
|
|
hasReloaded: () => this.send(MessageType.HasReloadedMenu),
|
|
}
|
|
|
|
readonly spellchecker = {
|
|
manager: () => this.send(MessageType.SpellCheckerManager),
|
|
languages: () => this.send(MessageType.SpellCheckerLanguages),
|
|
}
|
|
|
|
readonly backups = {
|
|
enabled: (): Promise<boolean> => this.send(MessageType.BackupsAreEnabled),
|
|
toggleEnabled: (): Promise<boolean> => this.send(MessageType.ToggleBackupsEnabled),
|
|
location: (): Promise<string> => this.send(MessageType.BackupsLocation),
|
|
copyDecryptScript: async (location: string) => {
|
|
await this.send(MessageType.CopyDecryptScript, location)
|
|
},
|
|
changeLocation: (location: string) => this.send(MessageType.ChangeBackupsLocation, location),
|
|
save: (data: any) => this.send(MessageType.DataArchive, data),
|
|
perform: async () => {
|
|
await this.windowLoaded
|
|
await this.send(MessageType.PerformBackup)
|
|
await this.waitOn(AppMessageType.SavedBackup)
|
|
},
|
|
}
|
|
|
|
readonly updates = {
|
|
state: (): Promise<UpdateState> => this.send(MessageType.UpdateState),
|
|
autoUpdateEnabled: (): Promise<boolean> => this.send(MessageType.AutoUpdateEnabled),
|
|
check: () => this.send(MessageType.CheckForUpdate),
|
|
}
|
|
|
|
readonly net = {
|
|
getJSON: (url: string) => this.send(MessageType.GetJSON, url),
|
|
downloadFile: (url: string, filePath: string) => this.send(MessageType.DownloadFile, url, filePath),
|
|
}
|
|
|
|
stop = async () => {
|
|
this.appProcess.kill()
|
|
|
|
/** Give the process a little time before cleaning up */
|
|
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
|
|
/**
|
|
* Windows can throw EPERM or EBUSY errors when we try to delete the
|
|
* user data directory too quickly.
|
|
*/
|
|
const maxTries = 5
|
|
for (let i = 0; i < maxTries; i++) {
|
|
try {
|
|
await deleteDir(this.userDataPath)
|
|
return
|
|
} catch (error: any) {
|
|
if (error.code === 'EPERM' || error.code === 'EBUSY') {
|
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
throw new Error(`Couldn't delete user data directory after ${maxTries} tries`)
|
|
}
|
|
|
|
restart = async () => {
|
|
this.appProcess.kill()
|
|
this.appProcess = spawnAppprocess(this.userDataPath)
|
|
this.appProcess.on('message', this.receive)
|
|
this.appReady = this.waitOn(AppMessageType.Ready)
|
|
this.windowLoaded = this.waitOn(AppMessageType.WindowLoaded)
|
|
await this.appReady
|
|
}
|
|
}
|
|
|
|
export type { Driver }
|
|
|
|
export async function createDriver() {
|
|
const userDataPath = path.join(
|
|
__dirname,
|
|
'data',
|
|
'tmp',
|
|
`userData-${Date.now()}-${Math.round(Math.random() * 10000)}`,
|
|
)
|
|
await ensureDirectoryExists(userDataPath)
|
|
const driver = new Driver(userDataPath)
|
|
await driver.appReady
|
|
return driver
|
|
}
|