diff --git a/packages/core/src/generic_steps/git_clone.js b/packages/core/src/generic_steps/git_clone.js
index da0a3c2..1b857c8 100644
--- a/packages/core/src/generic_steps/git_clone.js
+++ b/packages/core/src/generic_steps/git_clone.js
@@ -33,6 +33,7 @@ export default async (pkg, step) => {
//`--depth ${step.depth ?? 1}`,
//"--filter=blob:none",
//"--filter=tree:0",
+ "--progress",
"--recurse-submodules",
"--remote-submodules",
step.url,
diff --git a/packages/core/src/handlers/apply.js b/packages/core/src/handlers/apply.js
index 7b9f47e..1270b84 100644
--- a/packages/core/src/handlers/apply.js
+++ b/packages/core/src/handlers/apply.js
@@ -83,6 +83,7 @@ export default async function apply(pkg_id, changes = {}) {
return pkg
} catch (error) {
global._relic_eventBus.emit(`pkg:error`, {
+ event: "apply",
id: pkg_id,
error
})
diff --git a/packages/core/src/handlers/execute.js b/packages/core/src/handlers/execute.js
index 4351d24..b22684b 100644
--- a/packages/core/src/handlers/execute.js
+++ b/packages/core/src/handlers/execute.js
@@ -19,6 +19,20 @@ export default async function execute(pkg_id, { useRemote = false, force = false
return false
}
+ if (pkg.last_status !== "installed") {
+ if (!force) {
+ BaseLog.info(`Package not installed [${pkg_id}], aborting execution`)
+
+ global._relic_eventBus.emit(`pkg:error`, {
+ id: pkg_id,
+ event: "execute",
+ error: new Error("Package not valid or not installed"),
+ })
+
+ return false
+ }
+ }
+
const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest
if (!fs.existsSync(manifestPath)) {
diff --git a/packages/core/src/handlers/install.js b/packages/core/src/handlers/install.js
index faf0919..e94274c 100644
--- a/packages/core/src/handlers/install.js
+++ b/packages/core/src/handlers/install.js
@@ -164,12 +164,13 @@ export default async function install(manifest) {
return pkg
} catch (error) {
global._relic_eventBus.emit(`pkg:error`, {
- id: id,
- error
+ id: id ?? manifest.constructor.id,
+ event: "install",
+ error,
})
global._relic_eventBus.emit(`pkg:update:state`, {
- id: id,
+ id: id ?? manifest.constructor.id,
last_status: "failed",
status_text: `Installation failed`,
})
diff --git a/packages/core/src/handlers/lastOperationRetry.js b/packages/core/src/handlers/lastOperationRetry.js
new file mode 100644
index 0000000..9da6b9b
--- /dev/null
+++ b/packages/core/src/handlers/lastOperationRetry.js
@@ -0,0 +1,76 @@
+import fs from "node:fs"
+import path from "node:path"
+
+import Logger from "../logger"
+import DB from "../db"
+
+import PackageInstall from "./install"
+import PackageUpdate from "./update"
+import PackageUninstall from "./uninstall"
+
+import Vars from "../vars"
+
+export default async function lastOperationRetry(pkg_id) {
+ try {
+ const Log = Logger.child({ service: `OPERATION_RETRY|${pkg_id}` })
+ const pkg = await DB.getPackages(pkg_id)
+
+ if (!pkg) {
+ Log.error(`This package doesn't exist`)
+ return null
+ }
+
+ Log.info(`Try performing last operation retry...`)
+
+ global._relic_eventBus.emit(`pkg:update:state`, {
+ id: pkg.id,
+ status_text: `Performing last operation retry...`,
+ })
+
+ switch (pkg.last_status) {
+ case "installing":
+ await PackageInstall(pkg.local_manifest)
+ break
+ case "updating":
+ await PackageUpdate(pkg_id)
+ break
+ case "uninstalling":
+ await PackageUninstall(pkg_id)
+ break
+ case "failed": {
+ // copy pkg.local_manifest to cache after uninstall
+ const cachedManifest = path.join(Vars.cache_path, path.basename(pkg.local_manifest))
+
+ await fs.promises.copyFile(pkg.local_manifest, cachedManifest)
+
+ await PackageUninstall(pkg_id)
+ await PackageInstall(cachedManifest)
+ break
+ }
+ default: {
+ Log.error(`Invalid last status: ${pkg.last_status}`)
+
+ global._relic_eventBus.emit(`pkg:error`, {
+ id: pkg.id,
+ event: "retrying last operation",
+ status_text: `Performing last operation retry...`,
+ })
+
+ return null
+ }
+ }
+
+ return pkg
+ } catch (error) {
+ Logger.error(`Failed to perform last operation retry of [${pkg_id}]`)
+ Logger.error(error)
+
+ global._relic_eventBus.emit(`pkg:error`, {
+ event: "retrying last operation",
+ id: pkg_id,
+ error: error,
+ })
+
+ return null
+ }
+}
\ No newline at end of file
diff --git a/packages/core/src/handlers/uninstall.js b/packages/core/src/handlers/uninstall.js
index b0ea282..f2c0984 100644
--- a/packages/core/src/handlers/uninstall.js
+++ b/packages/core/src/handlers/uninstall.js
@@ -20,21 +20,33 @@ export default async function uninstall(pkg_id) {
const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` })
Log.info(`Uninstalling package...`)
+
global._relic_eventBus.emit(`pkg:update:state`, {
id: pkg.id,
status_text: `Uninstalling package...`,
})
- const ManifestRead = await ManifestReader(pkg.local_manifest)
- const manifest = await ManifestVM(ManifestRead.code)
+ try {
+ const ManifestRead = await ManifestReader(pkg.local_manifest)
+ const manifest = await ManifestVM(ManifestRead.code)
- if (typeof manifest.uninstall === "function") {
- Log.info(`Performing uninstall hook...`)
- global._relic_eventBus.emit(`pkg:update:state`, {
+ if (typeof manifest.uninstall === "function") {
+ Log.info(`Performing uninstall hook...`)
+
+ global._relic_eventBus.emit(`pkg:update:state`, {
+ id: pkg.id,
+ status_text: `Performing uninstall hook...`,
+ })
+
+ await manifest.uninstall(pkg)
+ }
+ } catch (error) {
+ Log.error(`Failed to perform uninstall hook`, error)
+ global._relic_eventBus.emit(`pkg:error`, {
+ event: "uninstall",
id: pkg.id,
- status_text: `Performing uninstall hook...`,
+ error
})
- await manifest.uninstall(pkg)
}
Log.info(`Deleting package directory...`)
@@ -62,6 +74,7 @@ export default async function uninstall(pkg_id) {
return pkg
} catch (error) {
global._relic_eventBus.emit(`pkg:error`, {
+ event: "uninstall",
id: pkg_id,
error
})
diff --git a/packages/core/src/handlers/update.js b/packages/core/src/handlers/update.js
index 87d4ce0..ac7ca9d 100644
--- a/packages/core/src/handlers/update.js
+++ b/packages/core/src/handlers/update.js
@@ -116,10 +116,21 @@ export default async function update(pkg_id) {
return pkg
} catch (error) {
global._relic_eventBus.emit(`pkg:error`, {
+ event: "update",
id: pkg_id,
- error
+ error,
+ last_status: "failed"
})
+ try {
+ await DB.updatePackageById(pkg_id, {
+ last_status: "failed",
+ })
+ } catch (error) {
+ BaseLog.error(`Failed to update status of pkg [${pkg_id}]`)
+ BaseLog.error(error.stack)
+ }
+
BaseLog.error(`Failed to update package [${pkg_id}]`, error)
BaseLog.error(error.stack)
diff --git a/packages/core/src/index.js b/packages/core/src/index.js
index de39263..5440832 100644
--- a/packages/core/src/index.js
+++ b/packages/core/src/index.js
@@ -18,6 +18,7 @@ import PackageList from "./handlers/list"
import PackageRead from "./handlers/read"
import PackageAuthorize from "./handlers/authorize"
import PackageCheckUpdate from "./handlers/checkUpdate"
+import PackageLastOperationRetry from "./handlers/lastOperationRetry"
export default class RelicCore {
constructor(params) {
@@ -55,7 +56,8 @@ export default class RelicCore {
list: PackageList,
read: PackageRead,
authorize: PackageAuthorize,
- checkUpdate: PackageCheckUpdate
+ checkUpdate: PackageCheckUpdate,
+ lastOperationRetry: PackageLastOperationRetry,
}
openPath(pkg_id) {
diff --git a/packages/core/src/logger.js b/packages/core/src/logger.js
index 1f3d6d1..12bbc1a 100644
--- a/packages/core/src/logger.js
+++ b/packages/core/src/logger.js
@@ -1,4 +1,5 @@
import winston from "winston"
+import WinstonTransport from "winston-transport"
import colors from "cli-color"
const servicesToColor = {
@@ -6,10 +7,6 @@ const servicesToColor = {
color: "whiteBright",
background: "bgBlackBright",
},
- "INSTALL": {
- color: "whiteBright",
- background: "bgBlueBright",
- },
}
const paintText = (level, service, ...args) => {
@@ -27,6 +24,13 @@ const format = winston.format.printf(({ timestamp, service = "CORE", level, mess
return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}`
})
+class EventBusTransport extends WinstonTransport {
+ log(info, next) {
+ global._relic_eventBus.emit(`logger:new`, info)
+ next()
+ }
+}
+
export default winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
@@ -34,6 +38,7 @@ export default winston.createLogger({
),
transports: [
new winston.transports.Console(),
+ new EventBusTransport(),
//new winston.transports.File({ filename: "error.log", level: "error" }),
//new winston.transports.File({ filename: "combined.log" }),
],
diff --git a/packages/core/src/manifest/reader.js b/packages/core/src/manifest/reader.js
index 830278f..a34915f 100644
--- a/packages/core/src/manifest/reader.js
+++ b/packages/core/src/manifest/reader.js
@@ -39,6 +39,15 @@ export async function readManifest(manifest) {
throw new Error(`Manifest is not a file: ${target}`)
}
+ // copy to cache
+ const cachedManifest = path.join(Vars.cache_path, path.basename(target))
+
+ await fs.promises.copyFile(target, cachedManifest)
+
+ if (!fs.existsSync(cachedManifest)) {
+ throw new Error(`Manifest copy failed: ${target}`)
+ }
+
return {
remote_manifest: undefined,
local_manifest: target,
diff --git a/packages/gui/src/main/classes/CoreAdapter.js b/packages/gui/src/main/classes/CoreAdapter.js
index 9fa931d..12be69a 100644
--- a/packages/gui/src/main/classes/CoreAdapter.js
+++ b/packages/gui/src/main/classes/CoreAdapter.js
@@ -1,14 +1,76 @@
import sendToRender from "../utils/sendToRender"
+import { ipcMain } from "electron"
export default class CoreAdapter {
constructor(electronApp, RelicCore) {
this.app = electronApp
this.core = RelicCore
-
- this.initialize()
+ this.initialized = false
}
- events = {
+ loggerWindow = null
+
+ ipcEvents = {
+ "pkg:list": async () => {
+ return await this.core.package.list()
+ },
+ "pkg:get": async (event, pkg_id) => {
+ return await this.core.db.getPackages(pkg_id)
+ },
+ "pkg:read": async (event, manifest_path, options = {}) => {
+ const manifest = await this.core.package.read(manifest_path, options)
+
+ return JSON.stringify({
+ ...this.core.db.defaultPackageState({ ...manifest }),
+ ...manifest,
+ name: manifest.pkg_name,
+ })
+ },
+ "pkg:install": async (event, manifest_path) => {
+ return await this.core.package.install(manifest_path)
+ },
+ "pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => {
+ await this.core.package.update(pkg_id)
+
+ if (execOnFinish) {
+ await this.core.package.execute(pkg_id)
+ }
+
+ return true
+ },
+ "pkg:apply": async (event, pkg_id, changes) => {
+ return await this.core.package.apply(pkg_id, changes)
+ },
+ "pkg:uninstall": async (event, pkg_id) => {
+ return await this.core.package.uninstall(pkg_id)
+ },
+ "pkg:execute": async (event, pkg_id, { force = false } = {}) => {
+ // check for updates first
+ if (!force) {
+ const update = await this.core.package.checkUpdate(pkg_id)
+
+ if (update) {
+ return sendToRender("pkg:update_available", update)
+ }
+ }
+
+ return await this.core.package.execute(pkg_id)
+ },
+ "pkg:open": async (event, pkg_id) => {
+ return await this.core.openPath(pkg_id)
+ },
+ "pkg:last_operation_retry": async (event, pkg_id) => {
+ return await this.core.package.lastOperationRetry(pkg_id)
+ },
+ "pkg:cancel_current_operation": async (event, pkg_id) => {
+ return await this.core.package.cancelCurrentOperation(pkg_id)
+ },
+ "core:open-path": async (event, pkg_id) => {
+ return await this.core.openPath(pkg_id)
+ },
+ }
+
+ coreEvents = {
"pkg:new": (pkg) => {
sendToRender("pkg:new", pkg)
},
@@ -51,22 +113,53 @@ export default class CoreAdapter {
sendToRender(`new:notification`, {
type: "error",
message: `An error occurred`,
- description: `Something failed to ${data.event} package ${data.pkg_id}`,
+ description: `Something failed to ${data.event} package ${data.id}`,
})
sendToRender(`pkg:update:state`, data)
+ },
+ "logger:new": (data) => {
+ if (this.loggerWindow) {
+ this.loggerWindow.webContents.send("logger:new", data)
+ }
}
}
- initialize = () => {
- for (const [key, handler] of Object.entries(this.events)) {
+ attachLogger = (window) => {
+ this.loggerWindow = window
+
+ window.webContents.send("logger:new", {
+ timestamp: new Date().getTime(),
+ message: "Core adapter Logger attached",
+ })
+ }
+
+ initialize = async () => {
+ if (this.initialized) {
+ return
+ }
+
+ for (const [key, handler] of Object.entries(this.coreEvents)) {
global._relic_eventBus.on(key, handler)
}
+
+ for (const [key, handler] of Object.entries(this.ipcEvents)) {
+ ipcMain.handle(key, handler)
+ }
+
+ await this.core.initialize()
+ await this.core.setup()
+
+ this.initialized = true
}
deinitialize = () => {
- for (const [key, handler] of Object.entries(this.events)) {
+ for (const [key, handler] of Object.entries(this.coreEvents)) {
global._relic_eventBus.off(key, handler)
}
+
+ for (const [key, handler] of Object.entries(this.ipcEvents)) {
+ ipcMain.removeHandler(key, handler)
+ }
}
}
\ No newline at end of file
diff --git a/packages/gui/src/main/index.js b/packages/gui/src/main/index.js
index b8fc8e8..42bd7c2 100644
--- a/packages/gui/src/main/index.js
+++ b/packages/gui/src/main/index.js
@@ -26,62 +26,54 @@ const ProtocolRegistry = require("protocol-registry")
const protocolRegistryNamespace = "relic"
+class LogsViewer {
+ window = null
+
+ async createWindow() {
+ this.window = new BrowserWindow({
+ width: 800,
+ height: 600,
+ show: false,
+ resizable: true,
+ autoHideMenuBar: true,
+ icon: "../../resources/icon.png",
+ webPreferences: {
+ preload: path.join(__dirname, "../preload/index.js"),
+ sandbox: false,
+ },
+ })
+
+ if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
+ this.window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}/logs`)
+ } else {
+ this.window.loadFile(path.join(__dirname, "../renderer/index.html"))
+ }
+
+ await new Promise((resolve) => this.window.once("ready-to-show", resolve))
+
+ this.window.show()
+
+ return this.window
+ }
+
+ closeWindow() {
+ if (this.window) {
+ this.window.close()
+ }
+ }
+}
+
class ElectronApp {
constructor() {
- this.win = null
this.core = new RelicCore()
this.adapter = new CoreAdapter(this, this.core)
}
+ window = null
+
+ logsViewer = new LogsViewer()
+
handlers = {
- "pkg:list": async () => {
- return await this.core.package.list()
- },
- "pkg:get": async (event, pkg_id) => {
- return await this.core.db.getPackages(pkg_id)
- },
- "pkg:read": async (event, manifest_path, options = {}) => {
- const manifest = await this.core.package.read(manifest_path, options)
-
- return JSON.stringify({
- ...this.core.db.defaultPackageState({ ...manifest }),
- ...manifest,
- name: manifest.pkg_name,
- })
- },
- "pkg:install": async (event, manifest_path) => {
- return await this.core.package.install(manifest_path)
- },
- "pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => {
- await this.core.package.update(pkg_id)
-
- if (execOnFinish) {
- await this.core.package.execute(pkg_id)
- }
-
- return true
- },
- "pkg:apply": async (event, pkg_id, changes) => {
- return await this.core.package.apply(pkg_id, changes)
- },
- "pkg:uninstall": async (event, pkg_id) => {
- return await this.core.package.uninstall(pkg_id)
- },
- "pkg:execute": async (event, pkg_id, { force = false } = {}) => {
- // check for updates first
- if (!force) {
- const update = await this.core.package.checkUpdate(pkg_id)
-
- if (update) {
- return sendToRender("pkg:update_available", update)
- }
- }
-
- return await this.core.package.execute(pkg_id)
- },
- "pkg:open": async (event, pkg_id) => {
- return await this.core.openPath(pkg_id)
- },
"updater:check": () => {
autoUpdater.checkForUpdates()
},
@@ -90,22 +82,31 @@ class ElectronApp {
autoUpdater.quitAndInstall()
}, 3000)
},
- "settings:get": (e, key) => {
+ "settings:get": (event, key) => {
return global.SettingsStore.get(key)
},
- "settings:set": (e, key, value) => {
+ "settings:set": (event, key, value) => {
return global.SettingsStore.set(key, value)
},
- "settings:delete": (e, key) => {
+ "settings:delete": (event, key) => {
return global.SettingsStore.delete(key)
},
- "settings:has": (e, key) => {
+ "settings:has": (event, key) => {
return global.SettingsStore.has(key)
},
+ "app:open-logs": async (event) => {
+ const loggerWindow = await this.logsViewer.createWindow()
+
+ this.adapter.attachLogger(loggerWindow)
+
+ loggerWindow.webContents.send("logger:new", {
+ timestamp: new Date().getTime(),
+ message: "Logger opened, starting watching logs",
+ })
+ },
"app:init": async (event, data) => {
try {
- await this.core.initialize()
- await this.core.setup()
+ await this.adapter.initialize()
return {
pkg: pkg,
@@ -126,19 +127,8 @@ class ElectronApp {
}
}
- events = {
- "open-runtime-path": () => {
- return this.core.openPath()
- },
- "open-dev-logs": () => {
- return sendToRender("new:message", {
- message: "Not implemented yet",
- })
- }
- }
-
createWindow() {
- this.win = global.win = new BrowserWindow({
+ this.window = global.mainWindow = new BrowserWindow({
width: 450,
height: 670,
show: false,
@@ -151,20 +141,20 @@ class ElectronApp {
}
})
- this.win.on("ready-to-show", () => {
- this.win.show()
+ this.window.on("ready-to-show", () => {
+ this.window.show()
})
- this.win.webContents.setWindowOpenHandler((details) => {
+ this.window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: "deny" }
})
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
- this.win.loadURL(process.env["ELECTRON_RENDERER_URL"])
+ this.window.loadURL(process.env["ELECTRON_RENDERER_URL"])
} else {
- this.win.loadFile(path.join(__dirname, "../renderer/index.html"))
+ this.window.loadFile(path.join(__dirname, "../renderer/index.html"))
}
}
@@ -206,12 +196,12 @@ class ElectronApp {
event.preventDefault()
// Someone tried to run a second instance, we should focus our window.
- if (this.win) {
- if (this.win.isMinimized()) {
- this.win.restore()
+ if (this.window) {
+ if (this.window.isMinimized()) {
+ this.window.restore()
}
- this.win.focus()
+ this.window.focus()
}
console.log(`Second instance >`, commandLine)
@@ -235,10 +225,6 @@ class ElectronApp {
ipcMain.handle(key, this.handlers[key])
}
- for (const key in this.events) {
- ipcMain.on(key, this.events[key])
- }
-
app.on("second-instance", this.handleOnSecondInstance)
app.on("open-url", (event, url) => {
@@ -308,4 +294,4 @@ class ElectronApp {
}
}
-new ElectronApp().initialize()
+new ElectronApp().initialize()
\ No newline at end of file
diff --git a/packages/gui/src/main/utils/sendToRender.js b/packages/gui/src/main/utils/sendToRender.js
index 6fc3784..df4266f 100644
--- a/packages/gui/src/main/utils/sendToRender.js
+++ b/packages/gui/src/main/utils/sendToRender.js
@@ -32,7 +32,7 @@ export default (event, data) => {
return copy
}
- global.win.webContents.send(event, serializeIpc(data))
+ global.mainWindow.webContents.send(event, serializeIpc(data))
} catch (error) {
console.error(error)
}
diff --git a/packages/gui/src/renderer/src/App.jsx b/packages/gui/src/renderer/src/App.jsx
index 3323cb8..02f26ee 100644
--- a/packages/gui/src/renderer/src/App.jsx
+++ b/packages/gui/src/renderer/src/App.jsx
@@ -12,14 +12,19 @@ import AppDrawer from "layout/components/Drawer"
import { InternalRouter, PageRender } from "./router.jsx"
+import CrashError from "components/Crash"
+import LogsViewer from "./pages/logs"
+
// create a global app context
window.app = GlobalApp
class App extends React.Component {
state = {
- initializing: true,
pkg: null,
+ crash: null,
+ initializing: true,
+
appSetup: {
error: false,
installed: false,
@@ -98,6 +103,15 @@ class App extends React.Component {
console.log(`React version > ${versions["react"]}`)
console.log(`DOMRouter version > ${versions["react-router-dom"]}`)
+ //check if path is /logs
+ if (window.location.pathname === "/logs") {
+ return await this.setState({
+ initializing: false,
+ no_layout: true,
+ log_viewer_mode: true,
+ })
+ }
+
window.app.style.appendClassname("initializing")
for (const event in this.ipcEvents) {
@@ -133,17 +147,34 @@ class App extends React.Component {
algorithm: antd.theme.darkAlgorithm
}}
>
-
}
-
+ {log.message ?? "No message"} +
+No logs
+ } + + { + timeline.map((log) =>