import { Plugin } from "frontblock-generic/Plugin"; import { socketioRPC } from "frontblock-generic/RPC"; import { GitUpdater, RepoFolderStatus } from "./GitUpdater"; import { FrontblockCherryPicker } from "git-cherrypicker"; import * as Logger from 'log4js' import * as path from "path"; import { FrontblockAdmin } from "./FrontblockAdmin"; import { TableDefiniton } from "frontblock-generic/Admin"; import { promises as fs, mkdirSync as mkdir } from "fs" var exec = require('child-process-promise').exec; Logger.configure({ appenders: { "admin/updatemanager": { type: 'stdout' }, //app: { type: 'file', filename: 'application.log' } }, categories: { default: { appenders: [ 'admin/updatemanager' ], level: 'debug' } } }) const logger = Logger.getLogger("admin/updatemanager") /* cool snibbet const SharedStatus = (privateStatus: Status) => (name: string) => (installed: boolean) => (outdated: boolean) => { return { name: name, installed: installed, outdated: outdated, ...privateStatus } } */ type SharedStatus = { name: string installed: boolean outdated: boolean } abstract class Extension{ constructor( protected name: string ){} /** * promises the generic Status object */ abstract async status():Promise /** * promises a boolean. * true => isInstalled should be true afterwards * false => an error happend and nothing was installed */ abstract async install():Promise /** * promises a boolean. * true => isInstalled should be false afterwards * false => the function was not able to bring this plugin into an uninstalled state!!! */ abstract async uninstall():Promise /** * promises a boolean. * true => an update was performed * false => no update was performed */ abstract async update(): Promise /** * promises a boolean * true => update would install a newer version * false => nothing would happen if update would be called */ abstract async isOutdated():Promise /** * promises a boolean * true => install would do nothing would it be called * false => install would try to install the newest extension */ abstract async isInstalled():Promise } export type FSStatus = SharedStatus & { prefix: string exists: boolean empty: boolean } export abstract class FSExtension extends Extension{ constructor( name: string, protected readonly prefix: string){ super(name) logger.debug('Creating extension directory', path.resolve(prefix, name)) mkdir(path.resolve(prefix, name)) } public async status(): Promise{ const installed = await this.isInstalled() const outdated = await this.isOutdated() const exists = await this.exists() const empty = await this.isEmpty() const status = { name: this.name, prefix: this.prefix, installed: installed, outdated: outdated, exists: exists, empty: empty } return status } public async isEmpty(): Promise{ return true //TODO } public async exists(): Promise{ try { await fs.access(path.resolve(this.prefix, this.name)) return true } catch (error) { return false } } } type NPMStatus = FSStatus & { version?: string, wanted?: string, latest?: string, location: string, } export class NPMExtension extends FSExtension{ constructor( name, public localPrefix, ){ super(name) } public async status(): Promise { const files = await fs.readdir(this.localPrefix) const empty: boolean = files.length === 0 if(empty){ return { exists: true, empty: true, name: this.name, location: this.localPrefix }; } const { _error, _stdout, _stderr } = await exec('npm outdated --prefix ' + this.localPrefix + " --json" + ' ' + this.name) const { error, stdout, stderr } = await exec('npm info --prefix ' + this.localPrefix + ' ' + this.name + " version") if(_error | error){ logger.warn( _stderr, stderr) return {exists: true, empty: false, name: this.name, location: this.localPrefix} } const statusJson:{latest:string, location: string, wanted:string} = JSON.parse(_stdout) return { exists: true, empty: false, name: this.name, location: this.localPrefix, version: stdout, latest: statusJson.latest, wanted: statusJson.wanted } } public async install(): Promise { logger.info("npm install", this.name, 'to', path.resolve(this.localPrefix, this.name)) await fs.mkdir(this.localPrefix, {recursive: true}) const { error, stdout, stderr } = await exec('npm install --prefix '+ this.localPrefix + ' ' + this.name) if (error !== null) { logger.warn('npm install', this.name, 'from prefix', this.localPrefix, 'stderr:', stderr) return false } else { logger.info('npm install', this.name, 'from prefix', this.localPrefix, 'stdout:', stdout) return true } } public async uninstall(): Promise { logger.info("npm uninstall", this.name, 'from', path.resolve(this.localPrefix, this.name)) const { error, stdout, stderr } = await exec('npm uninstall --prefix '+ this.localPrefix + ' ' + this.name) if (error !== null) { logger.warn('npm uninstall', this.name, 'from prefix', this.localPrefix, 'stderr:', stderr) return false } else { logger.info('npm uninstall', this.name, 'from prefix', this.localPrefix, 'stdout:', stdout) return true } } public async update(): Promise { logger.info("npm update", path.resolve(this.localPrefix, this.name)) const { error, stdout, stderr } = await exec('npm update --prefix '+ this.localPrefix + ' ' + this.name) if (error !== null) { logger.warn('npm update', this.name, 'from prefix', this.localPrefix, 'stderr:', stderr) return false } else { logger.info('npm update', this.name, 'from prefix', this.localPrefix, 'stdout:', stdout) return true } } public async isOutdated(): Promise { //TODO logger.info("npm outdated", path.resolve(this.localPrefix, this.name)) const { error, stdout, stderr } = await exec('npm list --prefix '+ this.localPrefix + ' --depth=0 | grep ' + this.name) if (error !== null) { logger.warn('npm outdated', this.name, 'from prefix', this.localPrefix, 'stderr:', stderr) return false } else { logger.info('npm outdated', this.name, 'from prefix', this.localPrefix, 'stdout:', stdout) return true } } isInstalled(): Promise { throw new Error("Method not implemented."); //TODO } } export type GitRef = string export type GitExtensionState = UnknownState | GitRef export class GitExtension extends Extension{ constructor( name:string, version = "latest", public localPrefix:string, ){ super(name, version) } public async install(): Promise { return 'unknown' } public async uninstall(): Promise { return 'unknown' } public async update(): Promise { return 'unknown' } public async isOutdated(): Promise { return true } } export class UpdateManager extends Plugin{ private loadedPlugins:{[name in string]:Plugin} = {} private dashboardUpdater:GitUpdater private adminUpdater:GitUpdater private pluginUpdaters:{[name in string]:GitUpdater} = {} constructor(admin:FrontblockAdmin){ super(admin, "UpdateManager") } getDefaultConfig(): {} { return {} } exportRPCs(): socketioRPC[] { return [{ name: 'installPlugin', func: async (name:string, force = false) => {return await this.installPlugin(name, force)}, type: 'call', visibility: 'private' },{ name: 'startPlugin', func: async (name:string) => {return await this.startPlugin(name)}, type: 'call', visibility: 'private' },{ name: 'updatePlugin', func: async (name) => {return await this.updatePlugin(name)}, type: 'call', visibility: 'private' },{ name: 'setPluginVersion', func: async (name, tag) => {return await this.setPluginVersion(name, tag)}, type: 'call', visibility: 'private' },{ name: 'updateDashboard', func: async () => {return await this.updateDashboard()}, type: 'call', visibility: 'private' },{ name: 'getLoadedPluginNames', func: async () => {return await this.getLoadedPluginNames()}, type: 'call', visibility: 'private' }] } getTableDefinitions(): TableDefiniton[] { return [] } async updatePlatformDependencies(){ logger.info("installing npm stuff") /* const res = await exec('npm install --prefix ./plugins knex sqlite3') logger.warn(res.stderr) logger.info(res.stdout) */ await Promise.all([ new NPMExtension("knex@0.19.2", "./plugins").install(), new NPMExtension("sqlite3@4.1.0", "./plugins").install() ]) } async updateAdmin(force:boolean = false){ this.adminUpdater = new GitUpdater("./dist") let status = await this.adminUpdater.getStatus() if(force || !status.remote || !status.remote.includes("fb-dist/admin") || !status.exists || status.empty || !status.currentTag){ logger.warn("Cloning fb-dist/admin into ./dist ..."+(force?" USING FORCE!":"")) status = await this.adminUpdater.cloneRepo("https://gitea.frontblock.me/fb-dist/admin.git", force) } return status } async updateFrontblockLib(){ logger.warn("Picking FrontblockLib...") await (new FrontblockCherryPicker("fb-dist/admin", "master", "FrontblockLib.js")).write("./static/FrontblockLib.js") } async updateDashboard(force:boolean = false):Promise{ this.dashboardUpdater = new GitUpdater("./static") let status = await this.dashboardUpdater.getStatus() if(force || !status.exists || status.empty || !status.currentTag){ logger.warn("Cloning fb-dist/dashboard into ./static ..."+(force?" USING FORCE!":"")) status = await this.dashboardUpdater.cloneRepo("https://gitea.frontblock.me/fb-dist/dashboard.git", force) } return status } async startPlugin(name:string):Promise{ if(!this.pluginUpdaters[name]) return false const status = await this.pluginUpdaters[name].getStatus() if(!status.exists || status.empty || !status.tags || status.tags.length === 0){ if(status.currentTag && !status.latestTag){ //git glitches sometimes if you check immediately after clone logger.warn("re-fetching tag for "+name+"...") return await this.startPlugin(name) } logger.error("Bad repo status", name, status) return false } if(this.loadedPlugins[name]){ logger.error("Plugin", name, "is already started") return false } let str = "../plugins/"+name+"/Plugin" const pluginClass = await eval('require')(str) const pluginObj = new pluginClass.default() await pluginObj.start() this.admin.addPlugin(pluginObj) this.loadedPlugins[name] = pluginObj return true } async updatePlugin(name:string):Promise{ if(!this.pluginUpdaters[name]) return false const status = await this.pluginUpdaters[name].getStatus() if(!status.exists || status.empty || !status.tags || status.tags.length === 0){ logger.error("Bad repo status", name, status) return false } if(status.currentTag == status.latestTag){ logger.warn(name, "already at latest tag") return false } if(this.loadedPlugins[name]){ logger.error("Plugin", name, "is running. Stop it first") return false } this.pluginUpdaters[name].checkoutTag(status.latestTag!) return true } async setPluginVersion(pluginName:string, tag:string):Promise{ const status = await this.pluginUpdaters[pluginName].getStatus() if(!status.exists || !status.tags || status.tags.length === 0 || !status.tags.includes(tag)){ logger.error("Bad repo status", pluginName, status) return status } return await this.pluginUpdaters[pluginName].checkoutTag(tag) } getLoadedPlugins():Plugin[]{ return Object.values(this.loadedPlugins) } getLoadedPluginNames():string[]{ return Object.keys(this.loadedPlugins) } }