/* eslint-disable @typescript-eslint/no-dynamic-delete */ import { createWorkerProxy, terminateWorker } from '@services/libs/workerAdapter'; import { dialog, shell } from 'electron'; import { backOff } from 'exponential-backoff'; import { copy, createSymlink, exists, mkdir, mkdirp, mkdirs, pathExists, readdir, readFile, remove } from 'fs-extra'; import { inject, injectable } from 'inversify'; import path from 'path'; import { Worker } from 'worker_threads'; // @ts-expect-error - Vite worker import with ?nodeWorker query import WikiWorkerFactory from './wikiWorker?nodeWorker'; import { container } from '@services/container'; import { WikiChannel } from '@/constants/channels'; import { TIDDLERS_PATH, TIDDLYWIKI_PACKAGE_FOLDER, TIDDLYWIKI_TEMPLATE_FOLDER_PATH } from '@/constants/paths'; import type { IAuthenticationService } from '@services/auth/interface'; import type { IGitService, IGitUserInfos } from '@services/git/interface'; import { i18n } from '@services/libs/i18n'; import { getWikiErrorLogFileName, logger, startWikiLogger } from '@services/libs/log'; import serviceIdentifier from '@services/serviceIdentifier'; import type { IViewService } from '@services/view/interface'; import type { IWindowService } from '@services/windows/interface'; import { WindowNames } from '@services/windows/WindowProperties'; import type { IWorkspace, IWorkspaceService } from '@services/workspaces/interface'; import { isWikiWorkspace } from '@services/workspaces/interface'; import type { IWorkspaceViewService } from '@services/workspacesView/interface'; import { Observable } from 'rxjs'; import type { IChangedTiddlers } from 'tiddlywiki'; import { AlreadyExistError, CopyWikiTemplateError, DoubleWikiInstanceError, HTMLCanNotLoadError, SubWikiSMainWikiNotExistError, WikiRuntimeError } from './error'; import type { IWikiService } from './interface'; import { WikiControlActions } from './interface'; import { getSubWikiPluginContent, updateSubWikiPluginContent } from './plugin/subWikiPlugin'; import type { ISubWikiPluginContent } from './plugin/subWikiPlugin'; import type { IStartNodeJSWikiConfigs, WikiWorker } from './wikiWorker'; import type { IpcServerRouteMethods, IpcServerRouteNames } from './wikiWorker/ipcServerRoutes'; import { LOG_FOLDER } from '@/constants/appPaths'; import { isDevelopmentOrTest } from '@/constants/environment'; import { isHtmlWiki } from '@/constants/fileNames'; import { defaultServerIP } from '@/constants/urls'; import type { IDatabaseService } from '@services/database/interface'; import type { IPreferenceService } from '@services/preferences/interface'; import type { ISyncService } from '@services/sync/interface'; import { wikiWorkerStartedEventName } from './constants'; import type { IWorkerWikiOperations } from './wikiOperations/executor/wikiOperationInServer'; import { getSendWikiOperationsToBrowser } from './wikiOperations/sender/sendWikiOperationsToBrowser'; import type { ISendWikiOperationsToBrowser } from './wikiOperations/sender/sendWikiOperationsToBrowser'; @injectable() export class Wiki implements IWikiService { constructor( @inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService, @inject(serviceIdentifier.Authentication) private readonly authService: IAuthenticationService, @inject(serviceIdentifier.Database) private readonly databaseService: IDatabaseService, ) { } public async getSubWikiPluginContent(mainWikiPath: string): Promise { return await getSubWikiPluginContent(mainWikiPath); } // handlers public async copyWikiTemplate(newFolderPath: string, folderName: string): Promise { logger.info('starting', { newFolderPath, folderName, function: 'copyWikiTemplate', }); try { await this.createWiki(newFolderPath, folderName); const entries = await readdir(path.join(newFolderPath, folderName)); logger.debug('completed', { newFolderPath, folderName, function: 'copyWikiTemplate', entries, }); } catch (error) { logger.error('failed', { error, newFolderPath, folderName, function: 'copyWikiTemplate', }); throw new CopyWikiTemplateError(`${(error as Error).message}, (${newFolderPath}, ${folderName})`); } } // key is same to workspace id, so we can get this worker by workspace id private wikiWorkers: Partial> = {}; private nativeWorkers: Partial> = {}; public getWorker(id: string): WikiWorker | undefined { return this.wikiWorkers[id]?.proxy; } private getNativeWorker(id: string): Worker | undefined { return this.wikiWorkers[id]?.nativeWorker; } private readonly wikiWorkerStartedEventTarget = new EventTarget(); public async startWiki(workspaceID: string, userName: string): Promise { const workspaceService = container.get(serviceIdentifier.Workspace); if (workspaceID === undefined) { logger.error('Try to start wiki, but workspace ID not provided', { workspaceID }); return; } const previousWorker = this.getWorker(workspaceID); if (previousWorker !== undefined) { logger.error(new DoubleWikiInstanceError(workspaceID).message, { stack: new Error('stack').stack?.replace('Error:', '') ?? 'no stack' }); await this.stopWiki(workspaceID); } // use Promise to handle worker callbacks const workspace = await workspaceService.get(workspaceID); if (workspace === undefined) { logger.error('Try to start wiki, but workspace not found', { workspace, workspaceID }); return; } if (!isWikiWorkspace(workspace)) { logger.error('Try to start wiki, but workspace is not a wiki workspace', { workspace, workspaceID }); return; } const { port, rootTiddler, readOnlyMode, tokenAuth, homeUrl, lastUrl, https, excludedPlugins, isSubWiki, wikiFolderLocation, name, enableHTTPAPI, authToken } = workspace; if (isSubWiki) { logger.error('Try to start wiki, but workspace is sub wiki', { workspace, workspaceID }); return; } // wiki server is about to boot, but our webview is just start loading, wait for `view.webContents.on('did-stop-loading'` to set this to false await workspaceService.updateMetaData(workspaceID, { isLoading: true }); if (tokenAuth && authToken) { logger.debug('getOneTimeAdminAuthTokenForWorkspaceSync', { tokenAuth: String(tokenAuth), authToken, function: 'startWiki', }); } const workerData: IStartNodeJSWikiConfigs = { authToken, constants: { TIDDLYWIKI_PACKAGE_FOLDER: String(TIDDLYWIKI_PACKAGE_FOLDER) }, enableHTTPAPI, excludedPlugins, homePath: wikiFolderLocation, https, isDev: isDevelopmentOrTest, openDebugger: process.env.DEBUG_WORKER === 'true', readOnlyMode, rootTiddler, tiddlyWikiHost: defaultServerIP, tiddlyWikiPort: port, tokenAuth, userName, }; logger.debug('initializing wikiWorker for workspace', { workspaceID, function: 'Wiki.startWiki', }); // Create native worker using Vite's ?nodeWorker import // eslint-disable-next-line @typescript-eslint/no-unsafe-call const nativeWorker = WikiWorkerFactory() as Worker; const worker = createWorkerProxy(nativeWorker); logger.debug(`wikiWorker initialized`, { function: 'Wiki.startWiki' }); this.wikiWorkers[workspaceID] = { proxy: worker, nativeWorker }; this.wikiWorkerStartedEventTarget.dispatchEvent(new Event(wikiWorkerStartedEventName(workspaceID))); const wikiLogger = startWikiLogger(workspaceID, name); const loggerMeta = { worker: 'NodeJSWiki', homePath: wikiFolderLocation }; await new Promise((resolve, reject) => { // Handle worker errors nativeWorker.on('error', (error: Error) => { wikiLogger.error(error.message, { function: 'Worker.error' }); reject(new WikiRuntimeError(error, name, false)); }); // Handle worker exit nativeWorker.on('exit', (code) => { delete this.wikiWorkers[workspaceID]; const warningMessage = `NodeJSWiki ${workspaceID} Worker stopped with code ${code}`; logger.info(warningMessage, loggerMeta); if (code !== 0) { reject(new Error(`Worker stopped with exit code ${code}`)); } else { resolve(); } }); // Handle worker messages (for logging) nativeWorker.on('message', (message: unknown) => { if (message && typeof message === 'object' && 'log' in message) { wikiLogger.info('Worker message', { data: message }); } }); // subscribe to the Observable that startNodeJSWiki returns, handle messages send by our code logger.debug('startWiki calling startNodeJSWiki in the main process', { function: 'wikiWorker.startNodeJSWiki' }); worker.startNodeJSWiki(workerData).subscribe(async (message) => { if (message.type === 'control') { await workspaceService.update(workspaceID, { lastNodeJSArgv: message.argv }, true); switch (message.actions) { case WikiControlActions.booted: { setTimeout(async () => { logger.info('resolved with control booted', { ...loggerMeta, message: message.message, workspaceID, function: 'startWiki', }); resolve(); }, 100); break; } case WikiControlActions.start: { if (message.message !== undefined) { logger.debug('WikiControlActions.start', { 'message.message': message.message, ...loggerMeta, workspaceID }); } break; } case WikiControlActions.listening: { // API server started, but we are using IPC to serve content now, so do nothing here. if (message.message !== undefined) { logger.info('WikiControlActions.listening ' + message.message, { ...loggerMeta, workspaceID }); } break; } case WikiControlActions.error: { const errorMessage = message.message ?? 'get WikiControlActions.error without message'; logger.error('rejected with control error', { ...loggerMeta, message, errorMessage, workspaceID, function: 'startWiki', }); await workspaceService.updateMetaData(workspaceID, { isLoading: false, didFailLoadErrorMessage: errorMessage }); // fix "message":"listen EADDRINUSE: address already in use 0.0.0.0:5212" if (errorMessage.includes('EADDRINUSE')) { const portChange = { port: port + 1, homeUrl: homeUrl.replace(`:${port}`, `:${port + 1}`), lastUrl: lastUrl?.replace(`:${port}`, `:${port + 1}`) ?? null, }; await workspaceService.update(workspaceID, portChange, true); reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, true, { ...workspace, ...portChange })); return; } reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, false, { ...workspace })); } } } else if (message.type === 'stderr' || message.type === 'stdout') { wikiLogger.info(message.message, { function: 'startNodeJSWiki' }); } }); }); void this.afterWikiStart(workspaceID); } private async afterWikiStart(workspaceID: string): Promise { const workspaceService = container.get(serviceIdentifier.Workspace); const workspace = await workspaceService.get(workspaceID); if (workspace === undefined) { logger.error('get workspace failed', { workspaceID, function: 'afterWikiStart' }); return; } if (!isWikiWorkspace(workspace)) { return; } const { isSubWiki, enableHTTPAPI } = workspace; if (!isSubWiki && enableHTTPAPI) { // Auto enable server filters if HTTP API is enabled. So this feature immediately available to 3rd party apps, reduce user confusion. await this.wikiOperationInServer(WikiChannel.addTiddler, workspaceID, [ '$:/config/Server/AllowAllExternalFilters', 'yes', ]); } } /** * Ensure you get a started worker. If not stated, it will await for it to start. * @param workspaceID */ private async getWorkerEnsure(workspaceID: string): Promise { let worker = this.getWorker(workspaceID); if (worker === undefined) { // wait for wiki worker started await new Promise(resolve => { this.wikiWorkerStartedEventTarget.addEventListener(wikiWorkerStartedEventName(workspaceID), () => { resolve(); }); }); } else { return worker; } worker = this.getWorker(workspaceID); if (worker === undefined) { const errorMessage = `Still no wiki for ${workspaceID} after wikiWorkerStartedEventTarget.addEventListener(wikiWorkerStartedEventName. No running worker, maybe tiddlywiki server in this workspace failed to start`; logger.error( errorMessage, { function: 'getWorkerEnsure', }, ); throw new Error(errorMessage); } return worker; } public async callWikiIpcServerRoute(workspaceID: string, route: NAME, ...arguments_: Parameters) { // don't log full `arguments_` here, it might contains huge text logger.debug(`callWikiIpcServerRoute get ${route}`, { workspaceID }); const worker = await this.getWorkerEnsure(workspaceID); logger.debug(`callWikiIpcServerRoute got worker`); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Argument of type 'string | string[] | ITiddlerFields | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'.ts(2345) const response = await worker[route](...arguments_); logger.debug(`callWikiIpcServerRoute returning response`, { route, code: response.statusCode }); return response; } public getWikiChangeObserver$(workspaceID: string): Observable { return new Observable((observer) => { const getWikiChangeObserverIIFE = async () => { const worker = await this.getWorkerEnsure(workspaceID); const observable = worker.getWikiChangeObserver(); observable.subscribe(observer); }; void getWikiChangeObserverIIFE(); }); } public async extractWikiHTML(htmlWikiPath: string, saveWikiFolderPath: string): Promise { // hope saveWikiFolderPath = ParentFolderPath + wikifolderPath // We want the folder where the WIKI is saved to be empty, and we want the input htmlWiki to be an HTML file even if it is a non-wikiHTML file. Otherwise the program will exit abnormally. // eslint-disable-next-line @typescript-eslint/no-unsafe-call const nativeWorker = WikiWorkerFactory() as Worker; const worker = createWorkerProxy(nativeWorker); try { if (!isHtmlWiki(htmlWikiPath)) { throw new HTMLCanNotLoadError(htmlWikiPath); } if (await exists(saveWikiFolderPath)) { throw new AlreadyExistError(saveWikiFolderPath); } await worker.extractWikiHTML(htmlWikiPath, saveWikiFolderPath, { TIDDLYWIKI_PACKAGE_FOLDER }); } catch (error) { const result = `${(error as Error).name} ${(error as Error).message}`; logger.error(result, { worker: 'NodeJSWiki', method: 'extractWikiHTML', htmlWikiPath, saveWikiFolderPath }); return result; } finally { // this worker is only for one time use. we will spawn a new one for starting wiki later. await terminateWorker(nativeWorker); } } public async packetHTMLFromWikiFolder(wikiFolderLocation: string, pathOfNewHTML: string): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-call const nativeWorker = WikiWorkerFactory() as Worker; const worker = createWorkerProxy(nativeWorker); try { await worker.packetHTMLFromWikiFolder(wikiFolderLocation, pathOfNewHTML, { TIDDLYWIKI_PACKAGE_FOLDER }); } finally { // this worker is only for one time use. we will spawn a new one for starting wiki later. await terminateWorker(nativeWorker); } } public async stopWiki(id: string): Promise { const worker = this.getWorker(id); const nativeWorker = this.getNativeWorker(id); if (worker === undefined || nativeWorker === undefined) { logger.warn(`No wiki for ${id}. No running worker, means maybe tiddlywiki server in this workspace failed to start`, { function: 'stopWiki', stack: new Error('stack').stack?.replace('Error:', '') ?? 'no stack', }); return; } const syncService = container.get(serviceIdentifier.Sync); syncService.stopIntervalSync(id); try { logger.debug(`worker.beforeExit for ${id}`); worker.beforeExit(); logger.debug(`terminateWorker for ${id}`); await terminateWorker(nativeWorker); } catch (error) { logger.error('wiki worker stop failed', { function: 'stopWiki', error }); } delete this.wikiWorkers[id]; logger.info(`Wiki-worker for ${id} stopped`, { function: 'stopWiki' }); } /** * Stop all worker_thread, use and await this before app.quit() */ public async stopAllWiki(): Promise { logger.debug('stopAllWiki', { function: 'stopAllWiki', }); const tasks = []; for (const id of Object.keys(this.wikiWorkers)) { tasks.push(this.stopWiki(id)); } await Promise.all(tasks); logger.info('All wiki workers are stopped', { function: 'stopAllWiki' }); } /** * Send message to UI via WikiChannel.createProgress * @param message will show in the UI */ private readonly logProgress = (message: string): void => { logger.info(message, { handler: WikiChannel.createProgress }); }; private readonly folderToContainSymlinks = 'subwiki'; /** * Link a sub wiki to a main wiki, this will create a shortcut folder from main wiki to sub wiki, so when saving files to that shortcut folder, you will actually save file to the sub wiki * We place symbol-link (short-cuts) in the tiddlers/subwiki/ folder, and ignore this folder in the .gitignore, so this symlink won't be commit to the git, as it contains computer specific path. * @param {string} mainWikiPath folderPath of a wiki as link's destination * @param {string} folderName sub-wiki's folder name * @param {string} newWikiPath sub-wiki's folder path */ public async linkWiki(mainWikiPath: string, folderName: string, subWikiPath: string): Promise { const mainWikiTiddlersFolderSubWikisPath = path.join(mainWikiPath, TIDDLERS_PATH, this.folderToContainSymlinks); const subwikiSymlinkPath = path.join(mainWikiTiddlersFolderSubWikisPath, folderName); try { try { await remove(subwikiSymlinkPath); } catch (_error: unknown) { void _error; } await mkdirp(mainWikiTiddlersFolderSubWikisPath); await createSymlink(subWikiPath, subwikiSymlinkPath, 'junction'); this.logProgress(i18n.t('AddWorkspace.CreateLinkFromSubWikiToMainWikiSucceed')); } catch (error: unknown) { throw new Error(i18n.t('AddWorkspace.CreateLinkFromSubWikiToMainWikiFailed', { subWikiPath, mainWikiTiddlersFolderPath: subwikiSymlinkPath, error })); } } private async createWiki(newFolderPath: string, folderName: string): Promise { this.logProgress(i18n.t('AddWorkspace.StartUsingTemplateToCreateWiki')); const newWikiPath = path.join(newFolderPath, folderName); if (!(await pathExists(newFolderPath))) { throw new Error(i18n.t('AddWorkspace.PathNotExist', { path: newFolderPath })); } if (!(await pathExists(TIDDLYWIKI_TEMPLATE_FOLDER_PATH))) { throw new Error(i18n.t('AddWorkspace.WikiTemplateMissing', { TIDDLYWIKI_TEMPLATE_FOLDER_PATH })); } if (await pathExists(newWikiPath)) { throw new Error(i18n.t('AddWorkspace.WikiExisted', { newWikiPath })); } try { await copy(TIDDLYWIKI_TEMPLATE_FOLDER_PATH, newWikiPath, { filter: (source: string, _destination: string) => { // keep xxx/template/wiki/.gitignore // keep xxx/template/wiki/.github // ignore xxx/template/wiki/.git // prevent copy wiki repo's .git folder if (source.endsWith('.git')) { return false; } // it will be copied if return true return true; }, }); } catch { throw new Error(i18n.t('AddWorkspace.CantCreateFolderHere', { newWikiPath })); } this.logProgress(i18n.t('AddWorkspace.WikiTemplateCopyCompleted') + newWikiPath); } public async createSubWiki(parentFolderLocation: string, folderName: string, subWikiFolderName: string, mainWikiPath: string, tagName = '', onlyLink = false): Promise { this.logProgress(i18n.t('AddWorkspace.StartCreatingSubWiki')); const newWikiPath = path.join(parentFolderLocation, folderName); if (!(await pathExists(parentFolderLocation))) { throw new Error(i18n.t('AddWorkspace.PathNotExist', { path: parentFolderLocation })); } if (!onlyLink) { if (await pathExists(newWikiPath)) { throw new Error(i18n.t('AddWorkspace.WikiExisted', { newWikiPath })); } try { await mkdirs(newWikiPath); } catch { throw new Error(i18n.t('AddWorkspace.CantCreateFolderHere', { newWikiPath })); } } this.logProgress(i18n.t('AddWorkspace.StartLinkingSubWikiToMainWiki')); await this.linkWiki(mainWikiPath, folderName, newWikiPath); if (typeof tagName === 'string' && tagName.length > 0) { this.logProgress(i18n.t('AddWorkspace.AddFileSystemPath')); updateSubWikiPluginContent(mainWikiPath, newWikiPath, { tagName, subWikiFolderName }); } this.logProgress(i18n.t('AddWorkspace.SubWikiCreationCompleted')); } public async removeWiki(wikiPath: string, mainWikiToUnLink?: string, onlyRemoveLink = false): Promise { if (mainWikiToUnLink !== undefined) { const subWikiName = path.basename(wikiPath); await shell.trashItem(path.join(mainWikiToUnLink, TIDDLERS_PATH, this.folderToContainSymlinks, subWikiName)); } if (!onlyRemoveLink) { await shell.trashItem(wikiPath); } } public async ensureWikiExist(wikiPath: string, shouldBeMainWiki: boolean): Promise { logger.debug('checking wiki folder', { wikiPath, shouldBeMainWiki, }); if (!(await pathExists(wikiPath))) { const error = i18n.t('AddWorkspace.PathNotExist', { path: wikiPath }); logger.error('path does not exist', { wikiPath, function: 'ensureWikiExist', }); throw new Error(error); } const wikiInfoPath = path.resolve(wikiPath, 'tiddlywiki.info'); const wikiInfoExists = await pathExists(wikiInfoPath); logger.debug('checked tiddlywiki.info', { wikiInfoPath, exists: wikiInfoExists, }); if (shouldBeMainWiki && !wikiInfoExists) { const entries = await readdir(wikiPath); logger.error('tiddlywiki.info missing', { wikiPath, wikiInfoPath, function: 'ensureWikiExist', entries, }); throw new Error(i18n.t('AddWorkspace.ThisPathIsNotAWikiFolder', { wikiPath, wikiInfoPath })); } const tiddlersPath = path.join(wikiPath, TIDDLERS_PATH); const tiddlersExists = await pathExists(tiddlersPath); logger.debug('checked tiddlers folder', { tiddlersPath, exists: tiddlersExists, }); if (shouldBeMainWiki && !tiddlersExists) { logger.error('tiddlers folder missing', { wikiPath, tiddlersPath, function: 'ensureWikiExist', }); throw new Error(i18n.t('AddWorkspace.ThisPathIsNotAWikiFolder', { wikiPath })); } logger.debug('validation passed', { wikiPath, function: 'ensureWikiExist', }); } public async checkWikiExist(workspace: IWorkspace, options: { shouldBeMainWiki?: boolean; showDialog?: boolean } = {}): Promise { if (!isWikiWorkspace(workspace)) { return true; // dedicated workspaces always "exist" } const { wikiFolderLocation, id: workspaceID } = workspace; const { shouldBeMainWiki, showDialog } = options; try { if (typeof wikiFolderLocation !== 'string' || wikiFolderLocation.length === 0 || !path.isAbsolute(wikiFolderLocation)) { const errorMessage = i18n.t('Dialog.NeedCorrectTiddlywikiFolderPath') + wikiFolderLocation; logger.error(errorMessage); const windowService = container.get(serviceIdentifier.Window); const mainWindow = windowService.get(WindowNames.main); if (mainWindow !== undefined && showDialog === true) { await dialog.showMessageBox(mainWindow, { title: i18n.t('Dialog.PathPassInCantUse'), message: errorMessage, buttons: ['OK'], cancelId: 0, defaultId: 0, }); } return errorMessage; } await this.ensureWikiExist(wikiFolderLocation, shouldBeMainWiki ?? false); return true; } catch (error) { const checkResult = (error as Error).message; const errorMessage = `${i18n.t('Dialog.CantFindWorkspaceFolderRemoveWorkspace')} ${wikiFolderLocation} ${checkResult}`; logger.error(errorMessage); const windowService = container.get(serviceIdentifier.Window); const mainWindow = windowService.get(WindowNames.main); if (mainWindow !== undefined && showDialog === true) { const { response } = await dialog.showMessageBox(mainWindow, { title: i18n.t('Dialog.WorkspaceFolderRemoved'), message: errorMessage, buttons: [i18n.t('Dialog.RemoveWorkspace'), i18n.t('Dialog.DoNotCare')], cancelId: 1, defaultId: 0, }); if (response === 0) { const workspaceViewService = container.get(serviceIdentifier.WorkspaceView); await workspaceViewService.removeWorkspaceView(workspaceID); } } return errorMessage; } } public async cloneWiki(parentFolderLocation: string, wikiFolderName: string, gitRepoUrl: string, gitUserInfo: IGitUserInfos): Promise { this.logProgress(i18n.t('AddWorkspace.StartCloningWiki')); const newWikiPath = path.join(parentFolderLocation, wikiFolderName); if (!(await pathExists(parentFolderLocation))) { throw new Error(i18n.t('AddWorkspace.PathNotExist', { path: parentFolderLocation })); } if (await pathExists(newWikiPath)) { throw new Error(i18n.t('AddWorkspace.WikiExisted', { newWikiPath })); } try { await mkdir(newWikiPath); } catch { throw new Error(i18n.t('AddWorkspace.CantCreateFolderHere', { newWikiPath })); } const gitService = container.get(serviceIdentifier.Git); await gitService.clone(gitRepoUrl, path.join(parentFolderLocation, wikiFolderName), gitUserInfo); } public async cloneSubWiki( parentFolderLocation: string, wikiFolderName: string, mainWikiPath: string, gitRepoUrl: string, gitUserInfo: IGitUserInfos, tagName = '', ): Promise { this.logProgress(i18n.t('AddWorkspace.StartCloningSubWiki')); const newWikiPath = path.join(parentFolderLocation, wikiFolderName); if (!(await pathExists(parentFolderLocation))) { throw new Error(i18n.t('AddWorkspace.PathNotExist', { path: parentFolderLocation })); } if (await pathExists(newWikiPath)) { throw new Error(i18n.t('AddWorkspace.WikiExisted', { newWikiPath })); } try { await mkdir(newWikiPath); } catch { throw new Error(i18n.t('AddWorkspace.CantCreateFolderHere', { newWikiPath })); } const gitService = container.get(serviceIdentifier.Git); await gitService.clone(gitRepoUrl, path.join(parentFolderLocation, wikiFolderName), gitUserInfo); this.logProgress(i18n.t('AddWorkspace.StartLinkingSubWikiToMainWiki')); await this.linkWiki(mainWikiPath, wikiFolderName, path.join(parentFolderLocation, wikiFolderName)); if (typeof tagName === 'string' && tagName.length > 0) { this.logProgress(i18n.t('AddWorkspace.AddFileSystemPath')); updateSubWikiPluginContent(mainWikiPath, newWikiPath, { tagName, subWikiFolderName: wikiFolderName }); } } // wiki-startup.ts private justStartedWiki: Record = {}; public setWikiStartLockOn(id: string): void { this.justStartedWiki[id] = true; } public setAllWikiStartLockOff(): void { this.justStartedWiki = {}; } public checkWikiStartLock(id: string): boolean { return this.justStartedWiki[id] ?? false; } public async wikiStartup(workspace: IWorkspace): Promise { if (!isWikiWorkspace(workspace)) { return; } const { id, isSubWiki, name, mainWikiID } = workspace; const userName = await this.authService.getUserName(workspace); // if is main wiki if (isSubWiki) { // if is private repo wiki // if we are creating a sub-wiki just now, restart the main wiki to load content from private wiki if (typeof mainWikiID === 'string' && !this.checkWikiStartLock(mainWikiID)) { const workspaceService = container.get(serviceIdentifier.Workspace); const mainWorkspace = await workspaceService.get(mainWikiID); if (mainWorkspace === undefined) { throw new SubWikiSMainWikiNotExistError(name ?? id, mainWikiID); } await this.restartWiki(mainWorkspace); } } else { try { logger.debug('calling startWiki', { function: 'startWiki', }); await this.startWiki(id, userName); logger.debug('done', { function: 'startWiki', }); } catch (error) { logger.warn('startWiki failed', { function: 'startWiki', error }); if (error instanceof WikiRuntimeError && error.retry) { logger.warn('startWiki retry', { function: 'startWiki', error }); // don't want it to throw here again, so no await here. const workspaceViewService = container.get(serviceIdentifier.WorkspaceView); return workspaceViewService.restartWorkspaceViewService(id); } else if ((error as Error).message.includes('Did not receive an init message from worker after')) { // https://github.com/andywer/threads.js/issues/426 // wait some time and restart the wiki will solve this logger.warn('startWiki handle error, restarting', { function: 'startWiki', error }); await this.restartWiki(workspace); } else { logger.warn('unexpected error, throw it', { function: 'startWiki' }); throw error; } } } const syncService = container.get(serviceIdentifier.Sync); await syncService.startIntervalSyncIfNeeded(workspace); } public async restartWiki(workspace: IWorkspace): Promise { if (!isWikiWorkspace(workspace)) { return; } const { id, isSubWiki } = workspace; // use workspace specific userName first, and fall back to preferences' userName, pass empty editor username if undefined const userName = await this.authService.getUserName(workspace); const syncService = container.get(serviceIdentifier.Sync); syncService.stopIntervalSync(id); if (!isSubWiki) { await this.stopWiki(id); await this.startWiki(id, userName); } await syncService.startIntervalSyncIfNeeded(workspace); } public async updateSubWikiPluginContent(mainWikiPath: string, subWikiPath: string, newConfig?: IWorkspace, oldConfig?: IWorkspace): Promise { const newConfigTyped = newConfig && isWikiWorkspace(newConfig) ? newConfig : undefined; const oldConfigTyped = oldConfig && isWikiWorkspace(oldConfig) ? oldConfig : undefined; updateSubWikiPluginContent(mainWikiPath, subWikiPath, newConfigTyped, oldConfigTyped); } public async wikiOperationInBrowser( operationType: OP, workspaceID: string, arguments_: Parameters, ) { // At least wait for wiki started. Otherwise some services like theme may try to call this method even on app start. await this.getWorkerEnsure(workspaceID); const viewService = container.get(serviceIdentifier.View); await viewService.getLoadedViewEnsure(workspaceID, WindowNames.main); const sendWikiOperationsToBrowser = getSendWikiOperationsToBrowser(workspaceID); if (typeof sendWikiOperationsToBrowser[operationType] !== 'function') { throw new TypeError(`${operationType} gets no useful handler`); } if (!Array.isArray(arguments_)) { // TODO: better type handling here // eslint-disable-next-line @typescript-eslint/no-explicit-any throw new TypeError(`${(arguments_ as any) ?? ''} (${typeof arguments_}) is not a good argument array for ${operationType}`); } // @ts-expect-error A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556) this maybe a bug of ts... try remove this comment after upgrade ts. And the result become void is weird too. return await (sendWikiOperationsToBrowser[operationType](...arguments_) as unknown as ReturnType); } public async wikiOperationInServer( operationType: OP, workspaceID: string, arguments_: Parameters, ) { logger.debug(`Get ${operationType}`, { workspaceID, method: 'wikiOperationInServer' }); // This will never await if workspaceID isn't exist in user's workspace list. So prefer to check workspace existence before use this method. const worker = await this.getWorkerEnsure(workspaceID); logger.debug(`Get worker ${operationType}`, { workspaceID, hasWorker: worker !== undefined, method: 'wikiOperationInServer', arguments_ }); const result = await (worker.wikiOperation(operationType, ...arguments_) as unknown as ReturnType); logger.debug(`Get result ${operationType}`, { workspaceID, method: 'wikiOperationInServer' }); return result; } public async setWikiLanguage(workspaceID: string, tiddlywikiLanguageName: string): Promise { const twLanguageUpdateTimeout = 15_000; // no need to wait setting wiki language, this sometimes cause slow PC to fail on this step void backOff(async () => { await (this.wikiOperationInBrowser( WikiChannel.setTiddlerText, workspaceID, ['$:/language', tiddlywikiLanguageName, { timeout: twLanguageUpdateTimeout }], )); }, { startingDelay: 2000, }); } public async getTiddlerFilePath(title: string, workspaceID?: string): Promise { const workspaceService = container.get(serviceIdentifier.Workspace); const activeWorkspace = await workspaceService.getActiveWorkspace(); const wikiWorker = this.getWorker(workspaceID ?? activeWorkspace?.id ?? ''); if (wikiWorker !== undefined) { const tiddlerFileMetadata = wikiWorker.getTiddlerFileMetadata(title); if (tiddlerFileMetadata?.filepath !== undefined) { return tiddlerFileMetadata.filepath; } } } public async getWikiErrorLogs(workspaceID: string, wikiName: string): Promise<{ content: string; filePath: string }> { const filePath = path.join(LOG_FOLDER, getWikiErrorLogFileName(workspaceID, wikiName)); const content = await readFile(filePath, 'utf8'); return { content, filePath, }; } }