chore(electron): Use worker thread instead of separate process (#395)

This commit is contained in:
Yannis Petitot
2021-04-10 11:39:26 +02:00
committed by GitHub
parent bbac15421d
commit 897f0fed5a
15 changed files with 219 additions and 253 deletions
+1
View File
@@ -40,6 +40,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
extraResources
todo
conf.json
src/Bot/conf.js
-1
View File
@@ -2,5 +2,4 @@ dist
build
node_modules
assets
config
scripts
+35 -37
View File
@@ -1,58 +1,56 @@
const path = require('path');
const paths = require('./paths');
const webpack = require('webpack');
const getClientEnvironment = require('./env');
module.exports = mode => {
const env = getClientEnvironment('/');
return {
target: "electron-main",
target: 'electron-main',
mode,
entry: {
main: paths.electronIndexJs,
osuSongsScan: paths.osuSongsScan,
osuIsRunning: paths.osuIsRunning
osuIsRunning: paths.osuIsRunning,
},
output: {
path: paths.appBuild,
publicPath: paths.publicUrl,
filename: '[name].bundle.js'
filename: '[name].bundle.js',
},
node: {
__dirname: false
__dirname: false,
},
module: {
rules : [{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
sourceType: 'unambiguous',
presets: [
'@babel/preset-env'
],
}
},
{
test: /ipcMessages.js$/,
loader: 'string-replace-loader',
options:{
multiple: [
{
search: './processes/osuSongsScan.js',
replace: './osuSongsScan.bundle.js'
},
{
search: './processes/osuIsRunning.js',
replace: './osuIsRunning.bundle.js'
}
]
}
}]
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
sourceType: 'unambiguous',
presets: ['@babel/preset-env'],
},
},
{
test: /threads.*\.js$/,
loader: 'string-replace-loader',
options: {
multiple: [
{
search: './osuSongsScan.worker.js',
replace: '../../extraResources/osuSongsScan.bundle.js',
},
{
search: './osuIsRunning.worker.js',
replace: '../../extraResources/osuIsRunning.bundle.js',
},
],
},
},
],
},
plugins: [
new webpack.DefinePlugin(env.stringified),
],
}
};
plugins: [new webpack.DefinePlugin(env.stringified)],
};
};
+5 -11
View File
@@ -22,8 +22,7 @@ function ensureSlash(inputPath, needsSlash) {
}
}
const getPublicUrl = appPackageJson =>
envPublicUrl || require(appPackageJson).homepage;
const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage;
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
@@ -33,8 +32,7 @@ const getPublicUrl = appPackageJson =>
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
function getServedPath(appPackageJson) {
const publicUrl = getPublicUrl(appPackageJson);
const servedUrl =
envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
return ensureSlash(servedUrl, true);
}
@@ -54,9 +52,7 @@ const moduleFileExtensions = [
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find(extension =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
const extension = moduleFileExtensions.find(extension => fs.existsSync(resolveFn(`${filePath}.${extension}`)));
if (extension) {
return resolveFn(`${filePath}.${extension}`);
@@ -85,10 +81,8 @@ module.exports = {
publicUrl: getPublicUrl(resolveApp('package.json')),
servedPath: getServedPath(resolveApp('package.json')),
rendererWebpackConfig: resolveApp('config/renderer.webpack.config.js'),
osuSongsScan: resolveApp('./src/electron/processes/osuSongsScan.js'),
osuIsRunning: resolveApp('./src/electron/processes/osuIsRunning.js')
osuSongsScan: resolveApp('./src/electron/threads/osuSongsScan.worker.js'),
osuIsRunning: resolveApp('./src/electron/threads/osuIsRunning.worker.js'),
};
module.exports.moduleFileExtensions = moduleFileExtensions;
+5 -3
View File
@@ -11,8 +11,7 @@
"dev": "node scripts/start.js",
"start": "electron ./build/main.bundle.js",
"dev:win": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./scripts/start.ps1",
"prebuild": "node scripts/preBuild.js",
"build": "node scripts/build.js",
"build": "node scripts/preBuild.js && node scripts/build.js && node scripts/postBuild.js",
"test": "node scripts/test.js",
"lint": "yarn lint:prettier && yarn lint:es",
"lint:fix": "yarn lint:fix:prettier && yarn lint:fix:es",
@@ -46,6 +45,9 @@
},
"build": {
"appId": "io.beatconnect.client",
"extraResources": [
"./extraResources/**"
],
"publish": [
{
"provider": "github",
@@ -232,4 +234,4 @@
"react-app"
]
}
}
}
+16
View File
@@ -0,0 +1,16 @@
const { join } = require('path')
const { copyFileSync, emptyDirSync } = require('fs-extra')
const paths = require('../config/paths');
// Copy wallpaper binaries to public folder on build
const sources = [
'../build/osuIsRunning.bundle.js',
'../build/osuSongsScan.bundle.js',
]
const destFolder = join(__dirname, '..', 'extraResources');
emptyDirSync(destFolder);
sources.forEach(src => copyFileSync(join(__dirname,src), join(destFolder, src.split('/').pop())))
@@ -1,3 +1,5 @@
/* eslint-disable no-alert */
/* eslint-disable no-console */
import { ipcRenderer } from 'electron';
import { error } from 'electron-log';
import { useEffect, useState } from 'react';
@@ -11,38 +13,28 @@ import { scanOsuCollection } from './scanOsuCollections';
export const useOsuDbScan = () => {
const osuSongsPath = useSelector(getOsuSongPath);
const osuPath = useSelector(getOsuPath);
const { add: addTask, update, terminate } = useTasks();
const { add: addTask, terminate } = useTasks();
const history = useDownloadHistory();
const [isScanning, setIsScanning] = useState(false);
const scanOsuSongs = () => {
const scanOsuSongs = async () => {
if (isScanning) return;
if (!osuPath && !osuSongsPath) {
return alert('You need to select your osu! or songs folder before performing a scan');
alert('You need to select your osu! or songs folder before performing a scan');
return;
}
setIsScanning(true);
addTask({ name: 'Scanning beatmaps', status: 'running', description: '', section: 'Settings' });
ipcRenderer.send('osuSongsScan', { osuPath, osuSongsPath, allowLegacy: true }); // User osu folder path
ipcRenderer.on('osuSongsScanStatus', (e, args) => {
update({
name: 'Scanning beatmaps',
description: `${Math.round(args * 100)}%`,
});
});
ipcRenderer.on('osuSongsScanResults', (e, args) => {
terminate('Scanning beatmaps');
setIsScanning(false);
if (args.err) error(`Error while scannings song: ${args.err}`);
else {
history.set(args);
setLastScan({ date: Date.now(), beatmaps: Object.keys(args.beatmaps).length });
}
});
ipcRenderer.on('osuSongsScanError', (e, args) => {
terminate('Scanning beatmaps');
setIsScanning(false);
alert('Failed to scan beatmaps, check your songs and osu! path in settings section');
});
const result = await ipcRenderer.invoke('osuSongsScan', { osuPath });
terminate('Scanning beatmaps');
setIsScanning(false);
if (result.error) {
throw new Error(`Error while scannings song: ${result.error}`);
} else {
history.set(result);
setLastScan({ date: Date.now(), beatmaps: Object.keys(result.beatmaps).length });
}
};
return scanOsuSongs;
@@ -54,8 +46,13 @@ export const useOsuDbAutoScan = () => {
const osuPath = useSelector(getOsuPath);
useEffect(() => {
if (osuPath && osuSongsPath !== '') {
osuDbScan();
scanOsuCollection(osuPath).then(console.log);
osuDbScan()
.then(() => console.log('Osu db scan success!'))
.catch(err => {
error(`Error while scannings song: ${err.message}`);
alert('Failed to scan beatmaps, check your songs and osu! path in settings section');
});
scanOsuCollection(osuPath).then(() => console.log('Collection scan success!'));
}
}, []);
};
+13 -23
View File
@@ -2,27 +2,18 @@ const log = require('electron-log');
const { error } = require('electron-log');
const { ipcMain, dialog, shell } = require('electron');
const { join } = require('path');
const { fork } = require('child_process');
const { downloadAndSetWallpaper } = require('./wallpaper');
const { readCollectionDB } = require('./helpers/osuCollections/collections.utils');
const startPullingOsuState = require('./threads/osuIsRunning');
const scanOsuDb = require('./threads/osuSongsScan');
ipcMain.on('osuSongsScan', (event, options) => {
// TODO Replace with osu-db-parser module
const osuSongsScanProcess = fork(join(__dirname, './processes/osuSongsScan.js'), null, { silent: true });
osuSongsScanProcess.stdout.pipe(process.stdout);
osuSongsScanProcess.send(JSON.stringify({ msg: 'start', ...options }));
osuSongsScanProcess.on('message', msg => {
const { results, status, err, overallDuration, overallUnplayedCount } = JSON.parse(msg);
if (results) {
event.reply('osuSongsScanResults', { beatmaps: results, overallDuration, overallUnplayedCount });
osuSongsScanProcess.kill('SIGTERM');
}
if (status) event.reply('osuSongsScanStatus', status);
if (err) {
event.reply('osuSongsScanError', err);
osuSongsScanProcess.kill('SIGTERM');
}
});
ipcMain.handle('osuSongsScan', async (event, { osuPath }) => {
try {
const [beatmaps, overallDuration, overallUnplayedCount] = await scanOsuDb(`${osuPath}/osu!.db`);
return { beatmaps, overallDuration, overallUnplayedCount };
} catch (e) {
return { error: e.message };
}
});
ipcMain.on('set-wallpaper', (event, bgUri) => {
@@ -44,11 +35,10 @@ ipcMain.on('set-wallpaper', (event, bgUri) => {
ipcMain.on('start-osu', (event, osuPath) => shell.openPath(join(osuPath, 'osu!.exe')).catch(error));
ipcMain.once('start-pulling-osu-state', event => {
const osuIsRunningChecker = fork(join(__dirname, './processes/osuIsRunning.js'));
osuIsRunningChecker.on('message', msg => {
event.reply('osu-is-running', !!msg);
});
osuIsRunningChecker.send('start');
const osuStateHandler = isRunning => {
event.reply('osu-is-running', isRunning);
};
startPullingOsuState(osuStateHandler);
});
ipcMain.handle('scan-osu-collections', async (event, osuPath) => {
-31
View File
@@ -1,31 +0,0 @@
type callback('a) = (Js.Nullable.t(Js.Exn.t), 'a) => unit;
[@bs.module "fs"]
external readFile: (string, callback(Buffer.t)) => unit = "readFile";
[@bs.module "fs"]
external writeFile: (string, Buffer.t, callback(string)) => unit =
"writeFile";
[@bs.module "process"] external on: (string, string => unit) => unit = "on";
[@bs.module "process"] external send: string => unit = "send";
let osuDb =
readFile("/Users/yannis/Downloads/osu!.db", (err, buffer) =>
switch (Js.Nullable.toOption(err)) {
| Some(err) => Js.log(err)
| None => Js.log(OsuDbParser.read(buffer))
}
);
// let handleMessage = (message) =>
// switch (message) {
// | "start" => "Yep"
// | _ =>
// };
// on("message")
// TODO Handle incoming messages from main
// Send messages to main
// Handle those messages in main
// Send back datas from main to renderer
// All of this on reason via shared channel polymorphic variants and JSON converters
-27
View File
@@ -1,27 +0,0 @@
const { lookup } = require('ps-node');
const osuIsRuning = () => {
lookup(
{
command: 'osu!.exe',
},
(err, resultList) => {
if (err) {
throw new Error(err);
}
process.send(resultList.length);
},
);
};
let pullIntervalId = null;
process.on('message', msg => {
if (msg === 'start') {
pullIntervalId = setInterval(osuIsRuning, 15000);
}
});
process.once('SIGTERM', () => {
process.removeAllListeners();
if (pullIntervalId) clearInterval(pullIntervalId);
});
-94
View File
@@ -1,94 +0,0 @@
const path = require('path');
const fs = require('fs');
const { join } = require('path');
const { log } = require('electron-log');
const parser = require('../helpers/beatmapParser');
const { readOsuDB, winTickToMs } = require('../helpers/osudb');
const osuSongsScan = songsDirectoryPath =>
new Promise((resolve, reject) => {
try {
const output = {};
const beatmaps = fs.readdirSync(songsDirectoryPath);
const beatmapsCount = beatmaps.length;
beatmaps.forEach((beatmap, i) => {
if (i % 50 === 0) {
const progress = (i / beatmapsCount).toFixed(2);
process.send(JSON.stringify({ status: progress }));
}
const beatmapPath = path.join(songsDirectoryPath, beatmap);
const dirStats = fs.lstatSync(beatmapPath);
const isDirExists = fs.existsSync(beatmapPath) && dirStats.isDirectory();
if (isDirExists) {
const date = dirStats.mtimeMs;
const assets = fs.readdirSync(beatmapPath);
for (let j = 0; j < assets.length; j++) {
if (assets[j].split('.').pop() === 'osu') {
const data = fs.readFileSync(path.join(beatmapPath, assets[j]), 'utf8');
const { Metadata } = parser(data);
if (!(typeof Metadata === 'undefined')) {
const { BeatmapSetID, Title, Artist } = Metadata;
if (BeatmapSetID && BeatmapSetID !== '-1' && BeatmapSetID !== '0')
output[BeatmapSetID] = { id: BeatmapSetID, name: `${Title} | ${Artist}`, date };
break;
}
}
}
}
});
resolve(output);
} catch (err) {
reject(err);
}
});
const osuDbScan = osuPath => {
const re = readOsuDB(`${osuPath}/osu!.db`);
const out = {};
let overallDuration = 0;
let overallUnplayedCount = 0;
re.beatmaps.forEach(beatmap => {
if (beatmap.beatmapset_id === -1) return;
if (out[beatmap.beatmapset_id]) return;
if (beatmap.unplayed) overallUnplayedCount += 1;
overallDuration += beatmap.total_time;
out[beatmap.beatmapset_id] = {
id: beatmap.beatmapset_id,
date: winTickToMs(beatmap.last_modification_time),
title: beatmap.song_title,
artist: beatmap.artist_name,
creator: beatmap.creator_name,
isUnplayed: beatmap.unplayed,
md5: beatmap.md5,
audioPath: join(beatmap.folder_name, beatmap.audio_file_name),
previewOffset: beatmap.preview_offset,
};
});
return [out, overallDuration, overallUnplayedCount];
};
process.on('message', async data => {
const { msg, osuPath, osuSongsPath, allowLegacy } = JSON.parse(data);
let beatmaps = [];
let overallDuration = 0;
let overallUnplayedCount = 0;
switch (msg) {
case 'start':
try {
if (osuPath) {
[beatmaps, overallDuration, overallUnplayedCount] = osuDbScan(osuPath);
}
// Fallback to direcrory scan if failed to read osu db
if (!Object.keys(beatmaps).length && allowLegacy) beatmaps = await osuSongsScan(osuSongsPath);
} catch (err) {
log.error(`OsuSongScan: ${JSON.stringify(err.message)}`);
process.send(JSON.stringify({ err: err.message }));
throw err;
}
process.send(JSON.stringify({ results: beatmaps, overallDuration, overallUnplayedCount }));
break;
default:
break;
}
});
+34
View File
@@ -0,0 +1,34 @@
const { Worker } = require('worker_threads');
const { error } = require('electron-log');
const { join } = require('path');
let worker;
let pullIntervalId;
const startPullingOsuState = handler => {
if (worker && pullIntervalId) {
throw new Error('startPullingOsuState was already called and is started to end it call the returned stop function');
}
worker = new Worker(join(__dirname, './osuIsRunning.worker.js'));
pullIntervalId = setInterval(() => worker.postMessage('check-osu-state'), 10000);
worker.on('message', data => {
switch (data[0]) {
case 'result':
handler(data[1]);
break;
case 'error':
error(`[osuIsRunning thread]: ${data[1]}`);
break;
default:
break;
}
});
const stopPullingOsuState = () => {
clearInterval(pullIntervalId);
worker.removeAllListeners();
worker.terminate();
};
return stopPullingOsuState;
};
module.exports = startPullingOsuState;
@@ -0,0 +1,22 @@
const { parentPort } = require('worker_threads');
const { lookup } = require('ps-node');
let isBusy = false;
parentPort.on('message', data => {
if (data === 'check-osu-state' && !isBusy) {
isBusy = true;
lookup(
{
command: 'osu!.exe',
},
(err, resultList) => {
if (err) {
parentPort.postMessage(['error', err.message]);
} else {
parentPort.postMessage(['result', !!resultList.length]);
}
isBusy = false;
},
);
}
});
+31
View File
@@ -0,0 +1,31 @@
const { Worker } = require('worker_threads');
const { join } = require('path');
const { error } = require('electron-log');
const scanOsuDb = osuDbPath =>
new Promise((resolve, reject) => {
const worker = new Worker(join(__dirname, './osuSongsScan.worker.js'));
const terminate = () => {
worker.removeAllListeners();
worker.terminate();
};
worker.on('message', data => {
switch (data[0]) {
case 'result':
terminate();
resolve(data[1]);
break;
case 'error':
terminate();
error(`[scanOsuDb thread]: ${data[1]}`);
reject(data[1]);
break;
default:
terminate();
break;
}
});
worker.postMessage(osuDbPath);
});
module.exports = scanOsuDb;
@@ -0,0 +1,34 @@
const { parentPort } = require('worker_threads');
const { join } = require('path');
const { readOsuDB, winTickToMs } = require('../helpers/osudb');
parentPort.on('message', osuDbPath => {
if (osuDbPath) {
try {
const re = readOsuDB(osuDbPath);
const beatmaps = {};
let overallDuration = 0;
let overallUnplayedCount = 0;
re.beatmaps.forEach(beatmap => {
if (beatmap.beatmapset_id === -1) return;
if (beatmaps[beatmap.beatmapset_id]) return;
if (beatmap.unplayed) overallUnplayedCount += 1;
overallDuration += beatmap.total_time;
beatmaps[beatmap.beatmapset_id] = {
id: beatmap.beatmapset_id,
date: winTickToMs(beatmap.last_modification_time),
title: beatmap.song_title,
artist: beatmap.artist_name,
creator: beatmap.creator_name,
isUnplayed: beatmap.unplayed,
md5: beatmap.md5,
audioPath: join(beatmap.folder_name, beatmap.audio_file_name),
previewOffset: beatmap.preview_offset,
};
});
parentPort.postMessage(['result', [beatmaps, overallDuration, overallUnplayedCount]]);
} catch (e) {
parentPort.postMessage(['error', e.message]);
}
}
});