aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-08-03 15:08:36 +0200
committerChocobozzz <me@florianbigard.com>2022-08-03 15:08:36 +0200
commitc795e19663a93c24908a7318975f820bac63164f (patch)
tree149a303be38eedf5aa5a0ec02938f67c75a267fe
parentfd59208e8ccd796f9ad7e35db82d0c33acfcb92c (diff)
downloadPeerTube-c795e19663a93c24908a7318975f820bac63164f.tar.gz
PeerTube-c795e19663a93c24908a7318975f820bac63164f.tar.zst
PeerTube-c795e19663a93c24908a7318975f820bac63164f.zip
Automatically rebuild native modules on ABI change
-rw-r--r--server.ts6
-rw-r--r--server/helpers/utils.ts26
-rw-r--r--server/helpers/version.ts36
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/installer.ts7
-rw-r--r--server/initializers/migrations/0725-node-version.ts66
-rw-r--r--server/lib/plugins/plugin-manager.ts9
-rw-r--r--server/lib/plugins/yarn.ts5
-rw-r--r--server/lib/server-config-manager.ts2
-rw-r--r--server/models/application/application.ts22
-rw-r--r--server/tests/api/server/plugins.ts32
-rw-r--r--server/tests/fixtures/peertube-plugin-test-native/main.js21
-rw-r--r--server/tests/fixtures/peertube-plugin-test-native/package.json23
13 files changed, 227 insertions, 30 deletions
diff --git a/server.ts b/server.ts
index 35ccc6758..aaf1ea021 100644
--- a/server.ts
+++ b/server.ts
@@ -138,6 +138,7 @@ import { ServerConfigManager } from '@server/lib/server-config-manager'
138import { VideoViewsManager } from '@server/lib/views/video-views-manager' 138import { VideoViewsManager } from '@server/lib/views/video-views-manager'
139import { isTestOrDevInstance } from './server/helpers/core-utils' 139import { isTestOrDevInstance } from './server/helpers/core-utils'
140import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics' 140import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics'
141import { ApplicationModel } from '@server/models/application/application'
141 142
142// ----------- Command line ----------- 143// ----------- Command line -----------
143 144
@@ -330,12 +331,17 @@ async function startApplication () {
330 server.listen(port, hostname, async () => { 331 server.listen(port, hostname, async () => {
331 if (cliOptions.plugins) { 332 if (cliOptions.plugins) {
332 try { 333 try {
334 await PluginManager.Instance.rebuildNativePluginsIfNeeded()
335
333 await PluginManager.Instance.registerPluginsAndThemes() 336 await PluginManager.Instance.registerPluginsAndThemes()
334 } catch (err) { 337 } catch (err) {
335 logger.error('Cannot register plugins and themes.', { err }) 338 logger.error('Cannot register plugins and themes.', { err })
336 } 339 }
337 } 340 }
338 341
342 ApplicationModel.updateNodeVersions()
343 .catch(err => logger.error('Cannot update node versions.', { err }))
344
339 logger.info('HTTP server listening on %s:%d', hostname, port) 345 logger.info('HTTP server listening on %s:%d', hostname, port)
340 logger.info('Web server: %s', WEBSERVER.URL) 346 logger.info('Web server: %s', WEBSERVER.URL)
341 347
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 6b9333b53..5a4fe4fdd 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -4,7 +4,7 @@ import { join } from 'path'
4import { sha256 } from '@shared/extra-utils' 4import { sha256 } from '@shared/extra-utils'
5import { ResultList } from '@shared/models' 5import { ResultList } from '@shared/models'
6import { CONFIG } from '../initializers/config' 6import { CONFIG } from '../initializers/config'
7import { execPromise, execPromise2, randomBytesPromise } from './core-utils' 7import { randomBytesPromise } from './core-utils'
8import { logger } from './logger' 8import { logger } from './logger'
9 9
10function deleteFileAndCatch (path: string) { 10function deleteFileAndCatch (path: string) {
@@ -44,29 +44,6 @@ function getSecureTorrentName (originalName: string) {
44 return sha256(originalName) + '.torrent' 44 return sha256(originalName) + '.torrent'
45} 45}
46 46
47async function getServerCommit () {
48 try {
49 const tag = await execPromise2(
50 '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true',
51 { stdio: [ 0, 1, 2 ] }
52 )
53
54 if (tag) return tag.replace(/^v/, '')
55 } catch (err) {
56 logger.debug('Cannot get version from git tags.', { err })
57 }
58
59 try {
60 const version = await execPromise('[ ! -d .git ] || git rev-parse --short HEAD')
61
62 if (version) return version.toString().trim()
63 } catch (err) {
64 logger.debug('Cannot get version from git HEAD.', { err })
65 }
66
67 return ''
68}
69
70/** 47/**
71 * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns 48 * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns
72 * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does 49 * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does
@@ -88,7 +65,6 @@ export {
88 generateRandomString, 65 generateRandomString,
89 getFormattedObjects, 66 getFormattedObjects,
90 getSecureTorrentName, 67 getSecureTorrentName,
91 getServerCommit,
92 generateVideoImportTmpPath, 68 generateVideoImportTmpPath,
93 getUUIDFromFilename 69 getUUIDFromFilename
94} 70}
diff --git a/server/helpers/version.ts b/server/helpers/version.ts
new file mode 100644
index 000000000..5b3bf59dd
--- /dev/null
+++ b/server/helpers/version.ts
@@ -0,0 +1,36 @@
1import { execPromise, execPromise2 } from './core-utils'
2import { logger } from './logger'
3
4async function getServerCommit () {
5 try {
6 const tag = await execPromise2(
7 '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true',
8 { stdio: [ 0, 1, 2 ] }
9 )
10
11 if (tag) return tag.replace(/^v/, '')
12 } catch (err) {
13 logger.debug('Cannot get version from git tags.', { err })
14 }
15
16 try {
17 const version = await execPromise('[ ! -d .git ] || git rev-parse --short HEAD')
18
19 if (version) return version.toString().trim()
20 } catch (err) {
21 logger.debug('Cannot get version from git HEAD.', { err })
22 }
23
24 return ''
25}
26
27function getNodeABIVersion () {
28 const version = process.versions.modules
29
30 return parseInt(version)
31}
32
33export {
34 getServerCommit,
35 getNodeABIVersion
36}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 99ae64f8d..8165a289d 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 720 27const LAST_MIGRATION_VERSION = 725
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index 7d5919459..b02be9567 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -1,6 +1,8 @@
1import { ensureDir, readdir, remove } from 'fs-extra' 1import { ensureDir, readdir, remove } from 'fs-extra'
2import passwordGenerator from 'password-generator' 2import passwordGenerator from 'password-generator'
3import { join } from 'path' 3import { join } from 'path'
4import { isTestOrDevInstance } from '@server/helpers/core-utils'
5import { getNodeABIVersion } from '@server/helpers/version'
4import { UserRole } from '@shared/models' 6import { UserRole } from '@shared/models'
5import { logger } from '../helpers/logger' 7import { logger } from '../helpers/logger'
6import { buildUser, createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user' 8import { buildUser, createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user'
@@ -10,7 +12,6 @@ import { applicationExist, clientsExist, usersExist } from './checker-after-init
10import { CONFIG } from './config' 12import { CONFIG } from './config'
11import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION, RESUMABLE_UPLOAD_DIRECTORY } from './constants' 13import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION, RESUMABLE_UPLOAD_DIRECTORY } from './constants'
12import { sequelizeTypescript } from './database' 14import { sequelizeTypescript } from './database'
13import { isTestOrDevInstance } from '@server/helpers/core-utils'
14 15
15async function installApplication () { 16async function installApplication () {
16 try { 17 try {
@@ -175,7 +176,9 @@ async function createApplicationIfNotExist () {
175 logger.info('Creating application account.') 176 logger.info('Creating application account.')
176 177
177 const application = await ApplicationModel.create({ 178 const application = await ApplicationModel.create({
178 migrationVersion: LAST_MIGRATION_VERSION 179 migrationVersion: LAST_MIGRATION_VERSION,
180 nodeVersion: process.version,
181 nodeABIVersion: getNodeABIVersion()
179 }) 182 })
180 183
181 return createApplicationActor(application.id) 184 return createApplicationActor(application.id)
diff --git a/server/initializers/migrations/0725-node-version.ts b/server/initializers/migrations/0725-node-version.ts
new file mode 100644
index 000000000..d8b9cc750
--- /dev/null
+++ b/server/initializers/migrations/0725-node-version.ts
@@ -0,0 +1,66 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 const { transaction } = utils
10
11 {
12 const data = {
13 type: Sequelize.STRING,
14 defaultValue: null,
15 allowNull: true
16 }
17 await utils.queryInterface.addColumn('application', 'nodeVersion', data, { transaction })
18 }
19
20 {
21 const data = {
22 type: Sequelize.STRING,
23 defaultValue: null,
24 allowNull: true
25 }
26 await utils.queryInterface.addColumn('application', 'nodeABIVersion', data, { transaction })
27 }
28
29 {
30 const query = `UPDATE "application" SET "nodeVersion" = '${process.version}'`
31 await utils.sequelize.query(query, { transaction })
32 }
33
34 {
35 const nodeABIVersion = parseInt(process.versions.modules)
36 const query = `UPDATE "application" SET "nodeABIVersion" = ${nodeABIVersion}`
37 await utils.sequelize.query(query, { transaction })
38 }
39
40 {
41 const data = {
42 type: Sequelize.STRING,
43 defaultValue: null,
44 allowNull: false
45 }
46 await utils.queryInterface.changeColumn('application', 'nodeVersion', data, { transaction })
47 }
48
49 {
50 const data = {
51 type: Sequelize.STRING,
52 defaultValue: null,
53 allowNull: false
54 }
55 await utils.queryInterface.changeColumn('application', 'nodeABIVersion', data, { transaction })
56 }
57}
58
59function down (options) {
60 throw new Error('Not implemented.')
61}
62
63export {
64 up,
65 down
66}
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index a706df1e0..a46b97fa4 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -3,6 +3,7 @@ import { createReadStream, createWriteStream } from 'fs'
3import { ensureDir, outputFile, readJSON } from 'fs-extra' 3import { ensureDir, outputFile, readJSON } from 'fs-extra'
4import { basename, join } from 'path' 4import { basename, join } from 'path'
5import { decachePlugin } from '@server/helpers/decache' 5import { decachePlugin } from '@server/helpers/decache'
6import { ApplicationModel } from '@server/models/application/application'
6import { MOAuthTokenUser, MUser } from '@server/types/models' 7import { MOAuthTokenUser, MUser } from '@server/types/models'
7import { getCompleteLocale } from '@shared/core-utils' 8import { getCompleteLocale } from '@shared/core-utils'
8import { 9import {
@@ -23,7 +24,7 @@ import { PluginModel } from '../../models/server/plugin'
23import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins' 24import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPassOptions, RegisterServerOptions } from '../../types/plugins'
24import { ClientHtml } from '../client-html' 25import { ClientHtml } from '../client-html'
25import { RegisterHelpers } from './register-helpers' 26import { RegisterHelpers } from './register-helpers'
26import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' 27import { installNpmPlugin, installNpmPluginFromDisk, rebuildNativePlugins, removeNpmPlugin } from './yarn'
27 28
28export interface RegisteredPlugin { 29export interface RegisteredPlugin {
29 npmName: string 30 npmName: string
@@ -384,6 +385,12 @@ export class PluginManager implements ServerHook {
384 logger.info('Plugin %s uninstalled.', npmName) 385 logger.info('Plugin %s uninstalled.', npmName)
385 } 386 }
386 387
388 async rebuildNativePluginsIfNeeded () {
389 if (!await ApplicationModel.nodeABIChanged()) return
390
391 return rebuildNativePlugins()
392 }
393
387 // ###################### Private register ###################### 394 // ###################### Private register ######################
388 395
389 private async registerPluginOrTheme (plugin: PluginModel) { 396 private async registerPluginOrTheme (plugin: PluginModel) {
diff --git a/server/lib/plugins/yarn.ts b/server/lib/plugins/yarn.ts
index 3f45681d3..d105b95e0 100644
--- a/server/lib/plugins/yarn.ts
+++ b/server/lib/plugins/yarn.ts
@@ -31,11 +31,16 @@ async function removeNpmPlugin (name: string) {
31 await execYarn('remove ' + name) 31 await execYarn('remove ' + name)
32} 32}
33 33
34async function rebuildNativePlugins () {
35 await execYarn('install --pure-lockfile')
36}
37
34// ############################################################################ 38// ############################################################################
35 39
36export { 40export {
37 installNpmPlugin, 41 installNpmPlugin,
38 installNpmPluginFromDisk, 42 installNpmPluginFromDisk,
43 rebuildNativePlugins,
39 removeNpmPlugin 44 removeNpmPlugin
40} 45}
41 46
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts
index d16a88f65..a3312fa20 100644
--- a/server/lib/server-config-manager.ts
+++ b/server/lib/server-config-manager.ts
@@ -1,4 +1,4 @@
1import { getServerCommit } from '@server/helpers/utils' 1import { getServerCommit } from '@server/helpers/version'
2import { CONFIG, isEmailEnabled } from '@server/initializers/config' 2import { CONFIG, isEmailEnabled } from '@server/initializers/config'
3import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants' 3import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants'
4import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/lib/signup' 4import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/lib/signup'
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index a479de5d2..d4590e001 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -1,5 +1,6 @@
1import memoizee from 'memoizee' 1import memoizee from 'memoizee'
2import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript' 2import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
3import { getNodeABIVersion } from '@server/helpers/version'
3import { AttributesOnly } from '@shared/typescript-utils' 4import { AttributesOnly } from '@shared/typescript-utils'
4import { AccountModel } from '../account/account' 5import { AccountModel } from '../account/account'
5 6
@@ -37,6 +38,14 @@ export class ApplicationModel extends Model<Partial<AttributesOnly<ApplicationMo
37 @Column 38 @Column
38 latestPeerTubeVersion: string 39 latestPeerTubeVersion: string
39 40
41 @AllowNull(false)
42 @Column
43 nodeVersion: string
44
45 @AllowNull(false)
46 @Column
47 nodeABIVersion: number
48
40 @HasOne(() => AccountModel, { 49 @HasOne(() => AccountModel, {
41 foreignKey: { 50 foreignKey: {
42 allowNull: true 51 allowNull: true
@@ -52,4 +61,17 @@ export class ApplicationModel extends Model<Partial<AttributesOnly<ApplicationMo
52 static load () { 61 static load () {
53 return ApplicationModel.findOne() 62 return ApplicationModel.findOne()
54 } 63 }
64
65 static async nodeABIChanged () {
66 const application = await this.load()
67
68 return application.nodeABIVersion !== getNodeABIVersion()
69 }
70
71 static async updateNodeVersions () {
72 const application = await this.load()
73
74 application.nodeABIVersion = getNodeABIVersion()
75 application.nodeVersion = process.version
76 }
55} 77}
diff --git a/server/tests/api/server/plugins.ts b/server/tests/api/server/plugins.ts
index 8aa34fb15..3ae99dc2e 100644
--- a/server/tests/api/server/plugins.ts
+++ b/server/tests/api/server/plugins.ts
@@ -2,6 +2,8 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { pathExists, remove } from 'fs-extra'
6import { join } from 'path'
5import { testHelloWorldRegisteredSettings } from '@server/tests/shared' 7import { testHelloWorldRegisteredSettings } from '@server/tests/shared'
6import { wait } from '@shared/core-utils' 8import { wait } from '@shared/core-utils'
7import { HttpStatusCode, PluginType } from '@shared/models' 9import { HttpStatusCode, PluginType } from '@shared/models'
@@ -9,6 +11,7 @@ import {
9 cleanupTests, 11 cleanupTests,
10 createSingleServer, 12 createSingleServer,
11 killallServers, 13 killallServers,
14 makeGetRequest,
12 PeerTubeServer, 15 PeerTubeServer,
13 PluginsCommand, 16 PluginsCommand,
14 setAccessTokensToServers 17 setAccessTokensToServers
@@ -349,6 +352,35 @@ describe('Test plugins', function () {
349 await check() 352 await check()
350 }) 353 })
351 354
355 it('Should rebuild native modules on Node ABI change', async function () {
356 await command.install({ path: PluginsCommand.getPluginTestPath('-native') })
357
358 await makeGetRequest({
359 url: server.url,
360 path: '/plugins/test-native/router',
361 expectedStatus: HttpStatusCode.NO_CONTENT_204
362 })
363
364 const query = `UPDATE "application" SET "nodeABIVersion" = 1`
365 await server.sql.updateQuery(query)
366
367 const baseNativeModule = server.servers.buildDirectory(join('plugins', 'node_modules', 'a-native-example'))
368 await remove(join(baseNativeModule, 'build'))
369 await remove(join(baseNativeModule, 'prebuilds'))
370
371 await server.kill()
372 await server.run()
373
374 await pathExists(join(baseNativeModule, 'build'))
375 await pathExists(join(baseNativeModule, 'prebuilds'))
376
377 await makeGetRequest({
378 url: server.url,
379 path: '/plugins/test-native/router',
380 expectedStatus: HttpStatusCode.NO_CONTENT_204
381 })
382 })
383
352 after(async function () { 384 after(async function () {
353 await cleanupTests([ server ]) 385 await cleanupTests([ server ])
354 }) 386 })
diff --git a/server/tests/fixtures/peertube-plugin-test-native/main.js b/server/tests/fixtures/peertube-plugin-test-native/main.js
new file mode 100644
index 000000000..0390faea9
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-native/main.js
@@ -0,0 +1,21 @@
1const print = require('a-native-example')
2
3async function register ({ getRouter }) {
4 print('hello world')
5
6 const router = getRouter()
7
8 router.get('/', (req, res) => {
9 print('hello world')
10 res.sendStatus(204)
11 })
12}
13
14async function unregister () {
15 return
16}
17
18module.exports = {
19 register,
20 unregister
21}
diff --git a/server/tests/fixtures/peertube-plugin-test-native/package.json b/server/tests/fixtures/peertube-plugin-test-native/package.json
new file mode 100644
index 000000000..a6525720b
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-native/package.json
@@ -0,0 +1,23 @@
1{
2 "name": "peertube-plugin-test-native",
3 "version": "0.0.1",
4 "description": "Plugin test-native",
5 "engine": {
6 "peertube": ">=4.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {},
20 "dependencies": {
21 "a-native-example": "^1.0.0"
22 }
23}