418 lines
13 KiB
TypeScript
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)
|
|
}
|
|
} |