Rearrange

This commit is contained in:
Manav Rathi 2024-04-15 15:40:50 +05:30
parent 720e84ba1f
commit 0881212e4f
No known key found for this signature in database
7 changed files with 118 additions and 108 deletions

View file

@ -27,6 +27,7 @@ import { setupAutoUpdater } from "./main/services/app-update";
import autoLauncher from "./main/services/autoLauncher";
import { initWatcher } from "./main/services/chokidar";
import { userPreferences } from "./main/stores/user-preferences";
import { registerStreamProtocol } from "./main/stream";
import { isDev } from "./main/util";
/**
@ -91,24 +92,26 @@ const setupRendererServer = () => serveNextAt(rendererURL);
/**
* Register privileged schemes.
*
* We have two priviliged schemes:
* We have two privileged schemes:
*
* 1. "ente", used for serving our web app (@see {@link setupRendererServer}).
*
* 2. "stream", used for streaming IPC (@see {@link registerStreamProtocol}).
*
* Both of these need some privileges, however, the documentation for Electron's
* registerSchemesAsPrivileged says:
* [registerSchemesAsPrivileged](https://www.electronjs.org/docs/latest/api/protocol)
* says:
*
* > This method ... can be called only once.
* >
* > https://www.electronjs.org/docs/latest/api/protocol#protocolisprotocolregisteredscheme-deprecated
*
* The library we use for the "ente" scheme already calls it once when we invoke
* {@link setupRendererServer}. Luckily, in practice it seems that the last call
* wins, and we don't need to modify the next-electron-server to prevent it from
* calling registerSchemesAsPrivileged. Instead, both schemes get registered
* fine, but we do need to repeat what next-electron-server had done.
* The library we use for the "ente" scheme, next-electron-server, already calls
* it once when we invoke {@link setupRendererServer}.
*
* In practice calling it multiple times just causes the values to be
* overwritten, and the last call wins. So we don't need to modify
* next-electron-server to prevent it from calling registerSchemesAsPrivileged.
* Instead, we (a) repeat what next-electron-server had done here, and (b)
* ensure that we're called after {@link setupRendererServer}.
*/
const registerPrivilegedSchemes = () => {
protocol.registerSchemesAsPrivileged([
@ -264,32 +267,6 @@ const setupTrayItem = (mainWindow: BrowserWindow) => {
tray.setContextMenu(createTrayContextMenu(mainWindow));
};
/**
* Register a protocol handler that we use for streaming large files between the
* main process (node) and the renderer process (browser) layer.
*
* [Note: IPC streams]
*
* When running without node integration, there is no direct way to pass streams
* across IPC. And passing the entire contents of the file is not feasible for
* large video files because of the memory pressure the copying would entail.
*
* As an alternative, we register a custom protocol handler that can provided a
* bi-directional stream. The renderer can stream data to the node side by
* streaming the request. The node side can stream to the renderer side by
* streaming the response.
*
* See also: [Note: Transferring large amount of data over IPC]
*
* Depends on: {@link registerPrivilegedSchemes}
*/
const registerStreamProtocol = () => {
protocol.handle("stream", (request: Request) => {
log.info({ e: "Got incoming stream", request });
return new Response("", { status: 200 });
});
};
/**
* Older versions of our app used to maintain a cache dir using the main
* process. This has been deprecated in favor of using a normal web cache.
@ -349,16 +326,15 @@ const main = () => {
// Note that some Electron APIs can only be used after this event occurs.
app.on("ready", async () => {
mainWindow = await createMainWindow();
const watcher = initWatcher(mainWindow);
setupTrayItem(mainWindow);
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
setupTrayItem(mainWindow);
attachIPCHandlers();
attachFSWatchIPCHandlers(watcher);
attachFSWatchIPCHandlers(initWatcher(mainWindow));
registerStreamProtocol();
if (!isDev) setupAutoUpdater(mainWindow);
handleDownloads(mainWindow);
handleExternalLinks(mainWindow);
addAllowOriginHeader(mainWindow);
registerStreamProtocol();
try {
deleteLegacyDiskCacheDirIfExists();

View file

@ -1,9 +1,9 @@
/**
* @file file system related functions exposed over the context bridge.
*/
import { createWriteStream, existsSync } from "node:fs";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { Readable } from "node:stream";
import { writeStream } from "./stream";
export const fsExists = (path: string) => existsSync(path);
@ -23,69 +23,6 @@ export const fsReadTextFile = async (filePath: string) =>
export const fsWriteFile = (path: string, contents: string) =>
fs.writeFile(path, contents);
/**
* Write a (web) ReadableStream to a file at the given {@link filePath}.
*
* The returned promise resolves when the write completes.
*
* @param filePath The local filesystem path where the file should be written.
* @param readableStream A [web
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
*/
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
/**
* Convert a Web ReadableStream into a Node.js ReadableStream
*
* This can be used to, for example, write a ReadableStream obtained via
* `net.fetch` into a file using the Node.js `fs` APIs
*/
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
const reader = readableStream.getReader();
const rs = new Readable();
rs._read = async () => {
try {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
} catch (e) {
rs.emit("error", e);
}
};
return rs;
};
const writeNodeStream = async (
filePath: string,
fileStream: NodeJS.ReadableStream,
) => {
const writeable = createWriteStream(filePath);
fileStream.on("error", (error) => {
writeable.destroy(error); // Close the writable stream with an error
});
fileStream.pipe(writeable);
await new Promise((resolve, reject) => {
writeable.on("finish", resolve);
writeable.on("error", async (e: unknown) => {
if (existsSync(filePath)) {
await fs.unlink(filePath);
}
reject(e);
});
});
};
/* TODO: Audit below this */
export const saveStreamToDisk = writeStream;

View file

@ -2,7 +2,7 @@ import pathToFfmpeg from "ffmpeg-static";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { ElectronFile } from "../../types/ipc";
import { writeStream } from "../fs";
import { writeStream } from "../stream";
import log from "../log";
import { generateTempFilePath, getTempDirPath } from "../temp";
import { execAsync } from "../util";

View file

@ -2,7 +2,7 @@ import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "path";
import { CustomErrors, ElectronFile } from "../../types/ipc";
import { writeStream } from "../fs";
import { writeStream } from "../stream";
import log from "../log";
import { isPlatform } from "../platform";
import { generateTempFilePath } from "../temp";

View file

@ -11,7 +11,7 @@ import fs from "node:fs/promises";
import * as ort from "onnxruntime-node";
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
import { CustomErrors } from "../../types/ipc";
import { writeStream } from "../fs";
import { writeStream } from "../stream";
import log from "../log";
import { generateTempFilePath } from "../temp";
import { deleteTempFile } from "./ffmpeg";

View file

@ -15,7 +15,7 @@ import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import * as ort from "onnxruntime-node";
import { writeStream } from "../fs";
import { writeStream } from "../stream";
import log from "../log";
/**

View file

@ -0,0 +1,97 @@
/**
* @file stream data to-from renderer using a custom protocol handler.
*/
import { protocol } from "electron/main";
import { createWriteStream, existsSync } from "node:fs";
import fs from "node:fs/promises";
import { Readable } from "node:stream";
import log from "./log";
/**
* Register a protocol handler that we use for streaming large files between the
* main process (node) and the renderer process (browser) layer.
*
* [Note: IPC streams]
*
* When running without node integration, there is no direct way to pass streams
* across IPC. And passing the entire contents of the file is not feasible for
* large video files because of the memory pressure the copying would entail.
*
* As an alternative, we register a custom protocol handler that can provided a
* bi-directional stream. The renderer can stream data to the node side by
* streaming the request. The node side can stream to the renderer side by
* streaming the response.
*
* See also: [Note: Transferring large amount of data over IPC]
*
* Depends on {@link registerPrivilegedSchemes}.
*/
export const registerStreamProtocol = () => {
protocol.handle("stream", (request: Request) => {
log.info({ e: "Got incoming stream", request });
return new Response("", { status: 200 });
});
};
/**
* Write a (web) ReadableStream to a file at the given {@link filePath}.
*
* The returned promise resolves when the write completes.
*
* @param filePath The local filesystem path where the file should be written.
* @param readableStream A [web
* ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
*/
export const writeStream = (filePath: string, readableStream: ReadableStream) =>
writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream));
/**
* Convert a Web ReadableStream into a Node.js ReadableStream
*
* This can be used to, for example, write a ReadableStream obtained via
* `net.fetch` into a file using the Node.js `fs` APIs
*/
const convertWebReadableStreamToNode = (readableStream: ReadableStream) => {
const reader = readableStream.getReader();
const rs = new Readable();
rs._read = async () => {
try {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
} catch (e) {
rs.emit("error", e);
}
};
return rs;
};
const writeNodeStream = async (
filePath: string,
fileStream: NodeJS.ReadableStream,
) => {
const writeable = createWriteStream(filePath);
fileStream.on("error", (error) => {
writeable.destroy(error); // Close the writable stream with an error
});
fileStream.pipe(writeable);
await new Promise((resolve, reject) => {
writeable.on("finish", resolve);
writeable.on("error", async (e: unknown) => {
if (existsSync(filePath)) {
await fs.unlink(filePath);
}
reject(e);
});
});
};