X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Flib%2Fplugins%2Fplugin-manager.ts;h=9086a4c8e855b2343fb77ddb9f49262bd001a81b;hb=f86ff3a02642263ac64db5d5fba4059db0092561;hp=ba9814383f19eeb028b136a9f8ae77ce635e566c;hpb=302eba0d898e38dca14739486441c27c0be6c62f;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index ba9814383..9086a4c8e 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -1,19 +1,22 @@ -import decache from 'decache' -import * as express from 'express' +import express from 'express' import { createReadStream, createWriteStream } from 'fs' import { ensureDir, outputFile, readJSON } from 'fs-extra' +import { Server } from 'http' import { basename, join } from 'path' +import { decachePlugin } from '@server/helpers/decache' +import { ApplicationModel } from '@server/models/application/application' import { MOAuthTokenUser, MUser } from '@server/types/models' -import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' -import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' +import { getCompleteLocale } from '@shared/core-utils' import { - ClientScript, - PluginPackageJson, - PluginTranslationPaths as PackagePluginTranslations -} from '../../../shared/models/plugins/plugin-package-json.model' -import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' + ClientScriptJSON, + PluginPackageJSON, + PluginTranslation, + PluginTranslationPathsJSON, + RegisterServerHookOptions +} from '@shared/models' +import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' import { PluginType } from '../../../shared/models/plugins/plugin.type' -import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model' +import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server/server-hook.model' import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' @@ -22,8 +25,7 @@ import { PluginModel } from '../../models/server/plugin' import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins' import { ClientHtml } from '../client-html' import { RegisterHelpers } from './register-helpers' -import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' -import { getCompleteLocale } from '@shared/core-utils' +import { installNpmPlugin, installNpmPluginFromDisk, rebuildNativePlugins, removeNpmPlugin } from './yarn' export interface RegisteredPlugin { npmName: string @@ -37,7 +39,7 @@ export interface RegisteredPlugin { path: string staticDirs: { [name: string]: string } - clientScripts: { [name: string]: ClientScript } + clientScripts: { [name: string]: ClientScriptJSON } css: string[] @@ -66,9 +68,45 @@ export class PluginManager implements ServerHook { private hooks: { [name: string]: HookInformationValue[] } = {} private translations: PluginLocalesTranslations = {} + private server: Server + private constructor () { } + init (server: Server) { + this.server = server + } + + registerWebSocketRouter () { + this.server.on('upgrade', (request, socket, head) => { + // Check if it's a plugin websocket connection + // No need to destroy the stream when we abort the request + // Other handlers in PeerTube will catch this upgrade event too (socket.io, tracker etc) + + const url = request.url + + const matched = url.match(`/plugins/([^/]+)/([^/]+/)?ws(/.*)`) + if (!matched) return + + const npmName = PluginModel.buildNpmName(matched[1], PluginType.PLUGIN) + const subRoute = matched[3] + + const result = this.getRegisteredPluginOrTheme(npmName) + if (!result) return + + const routes = result.registerHelpers.getWebSocketRoutes() + + const wss = routes.find(r => r.route.startsWith(subRoute)) + if (!wss) return + + try { + wss.handler(request, socket, head) + } catch (err) { + logger.error('Exception in plugin handler ' + npmName, { err }) + } + }) + } + // ###################### Getters ###################### isRegistered (npmName: string) { @@ -215,8 +253,12 @@ export class PluginManager implements ServerHook { for (const hook of this.hooks[hookName]) { logger.debug('Running hook %s of plugin %s.', hookName, hook.npmName) - result = await internalRunHook(hook.handler, hookType, result, params, err => { - logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err }) + result = await internalRunHook({ + handler: hook.handler, + hookType, + result, + params, + onError: err => { logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err }) } }) } @@ -277,11 +319,20 @@ export class PluginManager implements ServerHook { logger.info('Regenerating registered plugin CSS to global file.') await this.regeneratePluginGlobalCSS() } + + ClientHtml.invalidCache() } // ###################### Installation ###################### - async install (toInstall: string, version?: string, fromDisk = false) { + async install (options: { + toInstall: string + version?: string + fromDisk?: boolean // default false + register?: boolean // default true + }) { + const { toInstall, version, fromDisk = false, register = true } = options + let plugin: PluginModel let npmName: string @@ -310,21 +361,31 @@ export class PluginManager implements ServerHook { uninstalled: false, peertubeEngine: packageJSON.engine.peertube }, { returning: true }) - } catch (err) { - logger.error('Cannot install plugin %s, removing it...', toInstall, { err }) - try { - await removeNpmPlugin(npmName) - } catch (err) { - logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err }) - } + logger.info('Successful installation of plugin %s.', toInstall) - throw err - } + if (register) { + await this.registerPluginOrTheme(plugin) + } + } catch (rootErr) { + logger.error('Cannot install plugin %s, removing it...', toInstall, { err: rootErr }) - logger.info('Successful installation of plugin %s.', toInstall) + if (npmName) { + try { + await this.uninstall({ npmName }) + } catch (err) { + logger.error('Cannot uninstall plugin %s after failed installation.', toInstall, { err }) + + try { + await removeNpmPlugin(npmName) + } catch (err) { + logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err }) + } + } + } - await this.registerPluginOrTheme(plugin) + throw rootErr + } return plugin } @@ -344,16 +405,23 @@ export class PluginManager implements ServerHook { // Unregister old hooks await this.unregister(npmName) - return this.install(toUpdate, version, fromDisk) + return this.install({ toInstall: toUpdate, version, fromDisk }) } - async uninstall (npmName: string) { + async uninstall (options: { + npmName: string + unregister?: boolean // default true + }) { + const { npmName, unregister = true } = options + logger.info('Uninstalling plugin %s.', npmName) - try { - await this.unregister(npmName) - } catch (err) { - logger.warn('Cannot unregister plugin %s.', npmName, { err }) + if (unregister) { + try { + await this.unregister(npmName) + } catch (err) { + logger.warn('Cannot unregister plugin %s.', npmName, { err }) + } } const plugin = await PluginModel.loadByNpmName(npmName) @@ -372,6 +440,12 @@ export class PluginManager implements ServerHook { logger.info('Plugin %s uninstalled.', npmName) } + async rebuildNativePluginsIfNeeded () { + if (!await ApplicationModel.nodeABIChanged()) return + + return rebuildNativePlugins() + } + // ###################### Private register ###################### private async registerPluginOrTheme (plugin: PluginModel) { @@ -392,7 +466,7 @@ export class PluginManager implements ServerHook { registerHelpers = result.registerStore } - const clientScripts: { [id: string]: ClientScript } = {} + const clientScripts: { [id: string]: ClientScriptJSON } = {} for (const c of packageJSON.clientScripts) { clientScripts[c.script] = c } @@ -413,14 +487,16 @@ export class PluginManager implements ServerHook { } await this.addTranslations(plugin, npmName, packageJSON.translations) + + ClientHtml.invalidCache() } - private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) { + private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJSON) { const npmName = PluginModel.buildNpmName(plugin.name, plugin.type) // Delete cache if needed const modulePath = join(pluginPath, packageJSON.library) - decache(modulePath) + decachePlugin(pluginPath, modulePath) const library: PluginLibrary = require(modulePath) if (!isLibraryCodeValid(library)) { @@ -431,8 +507,7 @@ export class PluginManager implements ServerHook { await ensureDir(registerOptions.peertubeHelpers.plugin.getDataDirectoryPath()) - library.register(registerOptions) - .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) + await library.register(registerOptions) logger.info('Add plugin %s CSS to global file.', npmName) @@ -443,7 +518,7 @@ export class PluginManager implements ServerHook { // ###################### Translations ###################### - private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) { + private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PluginTranslationPathsJSON) { for (const locale of Object.keys(translationPaths)) { const path = translationPaths[locale] const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path)) @@ -468,8 +543,6 @@ export class PluginManager implements ServerHook { // ###################### CSS ###################### private resetCSSGlobalFile () { - ClientHtml.invalidCache() - return outputFile(PLUGIN_GLOBAL_CSS_PATH, '') } @@ -477,8 +550,6 @@ export class PluginManager implements ServerHook { for (const cssPath of cssRelativePaths) { await this.concatFiles(join(pluginPath, cssPath), PLUGIN_GLOBAL_CSS_PATH) } - - ClientHtml.invalidCache() } private concatFiles (input: string, output: string) { @@ -514,7 +585,7 @@ export class PluginManager implements ServerHook { private getPackageJSON (pluginName: string, pluginType: PluginType) { const pluginPath = join(this.getPluginPath(pluginName, pluginType), 'package.json') - return readJSON(pluginPath) as Promise + return readJSON(pluginPath) as Promise } private getPluginPath (pluginName: string, pluginType: PluginType) { @@ -558,14 +629,14 @@ export class PluginManager implements ServerHook { if (!this.hooks[options.target]) this.hooks[options.target] = [] this.hooks[options.target].push({ - npmName: npmName, + npmName, pluginName: plugin.name, handler: options.handler, priority: options.priority || 0 }) } - const registerHelpers = new RegisterHelpers(npmName, plugin, onHookAdded.bind(this)) + const registerHelpers = new RegisterHelpers(npmName, plugin, this.server, onHookAdded.bind(this)) return { registerStore: registerHelpers, @@ -573,7 +644,7 @@ export class PluginManager implements ServerHook { } } - private sanitizeAndCheckPackageJSONOrThrow (packageJSON: PluginPackageJson, pluginType: PluginType) { + private sanitizeAndCheckPackageJSONOrThrow (packageJSON: PluginPackageJSON, pluginType: PluginType) { if (!packageJSON.staticDirs) packageJSON.staticDirs = {} if (!packageJSON.css) packageJSON.css = [] if (!packageJSON.clientScripts) packageJSON.clientScripts = []