Skip to content

Instantly share code, notes, and snippets.

@alza54
Created January 31, 2024 15:32
Show Gist options
  • Save alza54/23e4ff00afd2dec06119f6c1f2fe92b2 to your computer and use it in GitHub Desktop.
Save alza54/23e4ff00afd2dec06119f6c1f2fe92b2 to your computer and use it in GitHub Desktop.
Electron RPC communication service snippet
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
import { Observable } from 'rxjs';
import { ServiceTemplate, ServiceClass, ServiceMethod, ServiceBase } from './service';
@ServiceClass('emulator')
class EmulatorService extends ServiceBase implements ServiceTemplate {
private process!: ChildProcessWithoutNullStreams;
constructor () {
super();
}
@ServiceMethod('list')
public async listEmulators (): Promise<string[]> {
return new Promise((resolve, reject) => {
const emulator = spawn('emulator', ['-list-avds']);
let emulators: string[] = [];
emulator.stdout.on('data', (data) => {
emulators = data.toString().split('\n');
});
emulator.stderr.on('data', (data) => {
reject(data.toString());
});
emulator.on('close', () => {
resolve(emulators);
});
emulator.on('error', (err) => {
reject(err);
});
});
}
@ServiceMethod('start')
public startEmulator (event: Electron.IpcMainInvokeEvent, emulator: string): Observable<string> {
return new Observable((subscriber) => {
this.process = spawn('emulator', [
'-avd',
emulator,
'-http-proxy',
'http://localhost:8080',
'-writable-system'
]);
this.process.stdout.on('data', (data) => {
subscriber.next(data.toString());
});
this.process.stderr.on('data', (data) => {
subscriber.next(data.toString());
});
this.process.on('close', (code: number) => {
if (code === 0) {
subscriber.complete();
} else {
subscriber.error(
new Error(`Emulator exited with code ${code}`)
);
}
});
this.process.on('error', (error: Error) => {
subscriber.error(error);
});
return () => {
this.process.kill();
};
});
}
}
export { EmulatorService };
import { IpcRendererEvent, contextBridge, ipcRenderer } from 'electron';
import { Observable, defer } from 'rxjs';
import { buildChannelNames } from './services';
// simple example snippet
contextBridge.exposeInMainWorld('API', {
listEmulators: () => ipcRenderer.invoke('emulator:list'),
startEmulator: async (emulator: string) => {
return new Promise((resolve, reject) => {
ipcRenderer.invoke('emulator:start', emulator);
const $ = createIpcSender<string>('emulator:start');
$.subscribe({
next: (data) => {
console.log(data);
},
error: (err) => {
console.error(err);
reject(err);
},
complete: () => {
console.log('Emulator closed');
resolve(undefined);
}
});
});
}
});
export function createIpcSender<R>(channel: string) {
return defer(() => {
const channels = buildChannelNames(channel);
return new Observable<R>((observer) => {
const onNext = (_: IpcRendererEvent, args: R) => {
observer.next(args);
};
const onError = (_: IpcRendererEvent, args: R) => {
removeListeners();
observer.error(args);
};
const onComplete = () => {
removeListeners();
observer.complete();
};
const removeListeners = () => {
ipcRenderer.removeListener(channels.next, onNext);
ipcRenderer.removeListener(channels.error, onError);
ipcRenderer.removeListener(channels.complete, onComplete);
};
ipcRenderer.on(channels.next, onNext);
ipcRenderer.once(channels.error, onError);
ipcRenderer.once(channels.complete, onComplete);
return removeListeners;
});
});
}
/* eslint-disable @typescript-eslint/no-explicit-any */
import Electron from 'electron';
import { Observable } from 'rxjs';
export interface ServiceTemplate {
name: string;
}
export type HandlerType<T = unknown> = (event: Electron.IpcMainInvokeEvent, ...args: any[]) => Promise<T> | Observable<T>;
const ExposedMethods = Symbol('ExposedMethods');
export class ServiceHandler {
private static _instance: ServiceHandler;
private _services: ServiceBase[] = [];
private _ipcMain: Electron.IpcMain;
public static getInstance (): ServiceHandler {
if (!ServiceHandler._instance) {
ServiceHandler._instance = new ServiceHandler();
}
return ServiceHandler._instance;
}
public bindIpcMain (ipcMain: Electron.IpcMain): void {
this._ipcMain = ipcMain;
}
public registerService (service: ServiceBase): void {
this._services.push(service);
}
public registerRequestHandler (requestName: string, handler: HandlerType): void {
this._ipcMain.handle(requestName, handler);
}
}
export abstract class ServiceBase implements ServiceTemplate {
public name: string = '';
constructor () {
ServiceHandler.getInstance().registerService(this);
}
public executeHandler (
handlerName: string,
handler: HandlerType,
event: Electron.IpcMainInvokeEvent,
...args: any[]
): Promise<void> | any {
const result: unknown = handler.apply(this, [event, ...args]);
if (!(result instanceof Observable)) {
return result;
}
return new Promise((resolve, reject) => {
const channelNames = buildChannelNames(handlerName);
result.subscribe({
next: (data: any) => {
event.sender.send(channelNames.next, data);
},
error: (error: unknown) => {
const serializedError = serializeError(error);
event.sender.send(channelNames.error, serializedError);
reject(serializedError);
},
complete: () => {
event.sender.send(channelNames.complete);
resolve(undefined);
}
});
});
}
}
export function ServiceClass<
T extends { new(...args: any[]): ServiceBase }
>(name: string) {
return function (Base: T) {
return class extends Base {
constructor(...args: any[]) {
super(...args);
this.name = name;
const exposedMethods = Base.prototype[ExposedMethods];
if (exposedMethods) {
exposedMethods.forEach((handlerName: string, methodName: string) => {
ServiceHandler.getInstance().registerRequestHandler(
`${name}:${handlerName}`,
this.executeHandler.bind(
this,
`${name}:${handlerName}`,
(this as any)[methodName].bind(this)
)
);
});
}
}
};
};
}
export function buildChannelNames(channel: string) {
return {
source: channel,
next: `${channel}/next`,
error: `${channel}/error`,
complete: `${channel}/complete`,
};
}
export function serializeError (error: unknown): Record<string, string> {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
};
}
return {
message: String(error),
};
}
export function ServiceMethod(handlerName: string) {
return function (target: any, propertyName: string) {
target[ExposedMethods] = target[ExposedMethods] || new Map();
target[ExposedMethods].set(propertyName, handlerName);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment