Skip to content

Commit

Permalink
255: Error dialogs
Browse files Browse the repository at this point in the history
  • Loading branch information
John authored and John committed Oct 20, 2023
1 parent 0f5ef64 commit 0fbf78f
Show file tree
Hide file tree
Showing 22 changed files with 1,925 additions and 89 deletions.
53 changes: 29 additions & 24 deletions electron/core/plugin-manager/execution/facade.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Plugin from "./Plugin";
import { register } from "./activation-manager";
import plugins from "../../../../web/public/plugins/plugin.json"

/**
* @typedef {Object.<string, any>} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options}
Expand Down Expand Up @@ -65,19 +66,23 @@ export async function getActive() {
return;
}
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive();
return plgList.map(
(plugin) =>
new Plugin(
plugin.name,
plugin.url,
plugin.activationPoints,
plugin.active,
plugin.description,
plugin.version,
plugin.icon
)
);
if(window.pluggableElectronIpc){
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive();
return plgList.map(
(plugin) =>
new Plugin(
plugin.name,
plugin.url,
plugin.activationPoints,
plugin.active,
plugin.description,
plugin.version,
plugin.icon
)
);
}
return Promise.resolve(plugins);
}

/**
Expand All @@ -89,18 +94,18 @@ export async function registerActive() {
if (typeof window === "undefined") {
return;
}
// eslint-disable-next-line no-undef
const plgList = await window.pluggableElectronIpc.getActive();
plgList.forEach((plugin) =>
register(
new Plugin(
plugin.name,
plugin.url,
plugin.activationPoints,
plugin.active
// eslint-disable-next-line no-undef
const plgList = await getActive()
plgList.forEach((plugin) =>
register(
new Plugin(
plugin.name,
plugin.url,
plugin.activationPoints,
plugin.active
)
)
)
);
);
}

/**
Expand Down
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
"workspaces": {
"packages": [
"electron",
"web"
"web",
"server"
],
"nohoist": [
"electron",
"electron/**",
"web",
"web/**"
"web/**",
"server",
"server/**"
]
},
"scripts": {
Expand All @@ -31,7 +34,10 @@
"build:publish": "yarn build:web && yarn workspace jan build:publish",
"build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin",
"build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32",
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux"
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux",
"buid:web-plugins": "yarn build:plugins && yarn build:web && cp \"./plugins/data-plugin/dist/esm/index.js\" \"./web/public/plugins/data-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/public/plugins/inference-plugin\" && cp \"./plugins/model-management-plugin/dist/index.js\" \"./web/public/plugins/model-management-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/public/plugins/monitoring-plugin\"",
"server:dev": "yarn buid:web-plugins && cpx \"web/out/**\" \"server/renderer/\" && mkdir -p ./server/@janhq && cp -r ./plugins/* ./server/@janhq && yarn workspace server dev"

},
"devDependencies": {
"concurrently": "^8.2.1",
Expand Down
9 changes: 8 additions & 1 deletion plugins/data-plugin/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const dbs: Record<string, any> = {};
*/
function createCollection(name: string, schema?: { [key: string]: any }): Promise<void> {
return new Promise<void>((resolve) => {
const dbPath = path.join(app.getPath("userData"), "databases");
const dbPath = path.join(appPath(), "databases");
if (!fs.existsSync(dbPath)) fs.mkdirSync(dbPath);
const db = new PouchDB(`${path.join(dbPath, name)}`);
dbs[name] = db;
Expand Down Expand Up @@ -226,6 +226,13 @@ function findMany(
.then((data) => data.docs); // Return documents
}

function appPath() {
if (app) {
return app.getPath("userData");
}
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
}

module.exports = {
createCollection,
deleteCollection,
Expand Down
7 changes: 7 additions & 0 deletions plugins/inference-plugin/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ function killSubprocess() {
}
}

function appPath() {
if (app) {
return app.getPath("userData");
}
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
}

module.exports = {
initModel,
killSubprocess,
Expand Down
47 changes: 46 additions & 1 deletion plugins/model-management-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,54 @@ import {
downloadFile,
deleteFile,
store,
EventName,
events
} from "@janhq/core";
import { parseToModel } from "./helper";

const downloadModel = (product) => downloadFile(product.downloadUrl, product.fileName);
const downloadModel = (product) => {
downloadFile(product.downloadUrl, product.fileName);
checkDownloadProgress(product.fileName);
}

async function checkDownloadProgress(fileName: string) {
if (typeof window !== "undefined" && typeof (window as any).electronAPI === "undefined") {
const intervalId = setInterval(() => {
fetchDownloadProgress(fileName, intervalId);
}, 3000);
}
}

async function fetchDownloadProgress(fileName: string, intervalId: NodeJS.Timeout): Promise<any> {
const response = await fetch("/api/v1/downloadProgress", {
method: 'POST',
body: JSON.stringify({ fileName: fileName }),
headers: { 'Content-Type': 'application/json', 'Authorization': '' }
});

if (!response.ok) {
events.emit(EventName.OnDownloadError, null);
clearInterval(intervalId);
return;
}
const json = await response.json();
if (isEmptyObject(json)) {
if (!fileName) {
clearInterval(intervalId);
}
return;
}
if (json.success === true) {
events.emit(EventName.OnDownloadSuccess, json);
clearInterval(intervalId);
} else {
events.emit(EventName.OnDownloadUpdate, json);
}
}

function isEmptyObject(ojb: any): boolean {
return Object.keys(ojb).length === 0;
}

const deleteModel = (path) => deleteFile(path);

Expand Down Expand Up @@ -87,6 +131,7 @@ function getModelById(modelId: string): Promise<any> {

function onStart() {
store.createCollection("models", {});
checkDownloadProgress(null);
}

// Register all the above functions and objects with the relevant extension points
Expand Down
172 changes: 172 additions & 0 deletions server/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import express, { Express, Request, Response, NextFunction } from 'express'
import cors from "cors";
import { resolve } from "path";
import { unlink, createWriteStream } from "fs";
const progress = require("request-progress");
const path = require("path");
const request = require("request");

interface ProgressState {
percent: number;
speed: number;
size: {
total: number;
transferred: number;
};
time: {
elapsed: number;
remaining: number;
};
success?: boolean | undefined;
fileName: string;
}

const options: cors.CorsOptions = { origin: "*" };
const requiredModules: Record<string, any> = {};
const port = process.env.PORT || 4000;
const dataDir = __dirname;
type DownloadProgress = Record<string, ProgressState>;
const downloadProgress: DownloadProgress = {};
const app: Express = express()
app.use(express.static(dataDir + '/renderer'))
app.use(cors(options))
app.use(express.json());

/**
* Execute a plugin module function via API call
*
* @param modulePath path to module name to import
* @param method function name to execute. The methods "deleteFile" and "downloadFile" will call the server function {@link deleteFile}, {@link downloadFile} instead of the plugin function.
* @param args arguments to pass to the function
* @returns Promise<any>
*
*/
app.post('/api/v1/invokeFunction', (req: Request, res: Response, next: NextFunction): void => {
const method = req.body["method"];
const args = req.body["args"];
switch (method) {
case "deleteFile":
deleteFile(args).then(() => res.json(Object())).catch((err: any) => next(err));
break;
case "downloadFile":
downloadFile(args.downloadUrl, args.fileName).then(() => res.json(Object())).catch((err: any) => next(err));
break;
default:
const result = invokeFunction(req.body["modulePath"], method, args)
if (typeof result === "undefined") {
res.json(Object())
} else {
result?.then((result: any) => {
res.json(result)
}).catch((err: any) => next(err));
}
}
});

app.post('/api/v1/downloadProgress', (req: Request, res: Response): void => {
const fileName = req.body["fileName"];
if (fileName && downloadProgress[fileName]) {
res.json(downloadProgress[fileName])
return;
} else {
const obj = downloadingFile();
if (obj) {
res.json(obj)
return;
}
}
res.json(Object());
});

app.use((err: Error, req: Request, res: Response, next: NextFunction): void => {
res.status(500);
res.json({ error: err?.message ?? "Internal Server Error" })
});

app.listen(port, () => console.log(`Application is running on port ${port}`));


async function invokeFunction(modulePath: string, method: string, args: any): Promise<any> {
console.log(modulePath, method, args);
const module = require(/* webpackIgnore: true */ path.join(
dataDir,
"",
modulePath
));
requiredModules[modulePath] = module;
if (typeof module[method] === "function") {
return module[method](...args);
} else {
return Promise.resolve();
}
}

function downloadModel(downloadUrl: string, fileName: string): void {
const userDataPath = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")

const destination = resolve(userDataPath, fileName);
console.log("Download file", fileName, "to", destination);
progress(request(downloadUrl), {})
.on("progress", function (state: any) {
downloadProgress[fileName] = {
...state,
fileName,
success: undefined
};
console.log("downloading file", fileName, (state.percent * 100).toFixed(2) + '%');
})
.on("error", function (err: Error) {
downloadProgress[fileName] = {
...downloadProgress[fileName],
success: false,
fileName: fileName,
};
})
.on("end", function () {
downloadProgress[fileName] = {
...downloadProgress[fileName],
success: true,
fileName: fileName,
};
})
.pipe(createWriteStream(destination));
}

function deleteFile(filePath: string): Promise<void> {
const userDataPath = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")
const fullPath = resolve(userDataPath, filePath);
return new Promise((resolve, reject) => {
unlink(fullPath, function (err) {
if (err && err.code === "ENOENT") {
reject(Error(`File does not exist: ${err}`));
} else if (err) {
reject(Error(`File delete error: ${err}`));
} else {
console.log(`Delete file ${filePath} from ${fullPath}`)
resolve();
}
});
})
}

function downloadingFile(): ProgressState | undefined {
const obj = Object.values(downloadProgress).find(obj => obj && typeof obj.success === "undefined")
return obj
}


async function downloadFile(downloadUrl: string, fileName: string): Promise<void> {
return new Promise((resolve, reject) => {
const obj = downloadingFile();
if (obj) {
reject(Error(obj.fileName + " is being downloaded!"))
return;
};
(async () => {
downloadModel(downloadUrl, fileName);
})().catch(e => {
console.error("downloadModel", fileName, e);
});
resolve();
});
}
5 changes: 5 additions & 0 deletions server/nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"watch": [
"main.ts"
]
}
Loading

0 comments on commit 0fbf78f

Please sign in to comment.