Files
fb-admin/src/backend/UpdateManger.ts
T
2019-08-26 22:37:48 +02:00

418 lines
13 KiB
TypeScript

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 = <Status>
(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<Status>{
constructor(
protected name: string
){}
/**
* promises the generic Status object
*/
abstract async status():Promise<Status>
/**
* promises a boolean.
* true => isInstalled should be true afterwards
* false => an error happend and nothing was installed
*/
abstract async install():Promise<boolean>
/**
* 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<boolean>
/**
* promises a boolean.
* true => an update was performed
* false => no update was performed
*/
abstract async update(): Promise<boolean>
/**
* promises a boolean
* true => update would install a newer version
* false => nothing would happen if update would be called
*/
abstract async isOutdated():Promise<boolean>
/**
* 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<boolean>
}
export type FSStatus = SharedStatus & {
prefix: string
exists: boolean
empty: boolean
}
export abstract class FSExtension extends Extension<FSStatus>{
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<FSStatus>{
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<boolean>{
return true //TODO
}
public async exists(): Promise<boolean>{
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<NPMStatus>{
constructor(
name,
public localPrefix,
){
super(name)
}
public async status(): Promise<NPMStatus> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> { //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<boolean> {
throw new Error("Method not implemented."); //TODO
}
}
export type GitRef = string
export type GitExtensionState = UnknownState | GitRef
export class GitExtension extends Extension<GitExtensionState>{
constructor(
name:string,
version = "latest",
public localPrefix:string,
){
super(name, version)
}
public async install(): Promise<GitExtensionState> {
return 'unknown'
}
public async uninstall(): Promise<GitExtensionState> {
return 'unknown'
}
public async update(): Promise<GitExtensionState> {
return 'unknown'
}
public async isOutdated(): Promise<boolean> {
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<RepoFolderStatus>{
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<boolean>{
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<boolean>{
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<RepoFolderStatus>{
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<any>[]{
return Object.values(this.loadedPlugins)
}
getLoadedPluginNames():string[]{
return Object.keys(this.loadedPlugins)
}
}