aboutsummaryrefslogtreecommitdiffhomepage
path: root/apps/peertube-runner/src/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /apps/peertube-runner/src/shared
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'apps/peertube-runner/src/shared')
-rw-r--r--apps/peertube-runner/src/shared/config-manager.ts140
-rw-r--r--apps/peertube-runner/src/shared/http.ts67
-rw-r--r--apps/peertube-runner/src/shared/index.ts3
-rw-r--r--apps/peertube-runner/src/shared/ipc/index.ts2
-rw-r--r--apps/peertube-runner/src/shared/ipc/ipc-client.ts88
-rw-r--r--apps/peertube-runner/src/shared/ipc/ipc-server.ts61
-rw-r--r--apps/peertube-runner/src/shared/ipc/shared/index.ts2
-rw-r--r--apps/peertube-runner/src/shared/ipc/shared/ipc-request.model.ts15
-rw-r--r--apps/peertube-runner/src/shared/ipc/shared/ipc-response.model.ts15
-rw-r--r--apps/peertube-runner/src/shared/logger.ts12
10 files changed, 405 insertions, 0 deletions
diff --git a/apps/peertube-runner/src/shared/config-manager.ts b/apps/peertube-runner/src/shared/config-manager.ts
new file mode 100644
index 000000000..84a326a16
--- /dev/null
+++ b/apps/peertube-runner/src/shared/config-manager.ts
@@ -0,0 +1,140 @@
1import { parse, stringify } from '@iarna/toml'
2import envPaths from 'env-paths'
3import { ensureDir, pathExists, remove } from 'fs-extra/esm'
4import { readFile, writeFile } from 'fs/promises'
5import merge from 'lodash-es/merge.js'
6import { dirname, join } from 'path'
7import { logger } from '../shared/index.js'
8
9const paths = envPaths('peertube-runner')
10
11type Config = {
12 jobs: {
13 concurrency: number
14 }
15
16 ffmpeg: {
17 threads: number
18 nice: number
19 }
20
21 registeredInstances: {
22 url: string
23 runnerToken: string
24 runnerName: string
25 runnerDescription?: string
26 }[]
27}
28
29export class ConfigManager {
30 private static instance: ConfigManager
31
32 private config: Config = {
33 jobs: {
34 concurrency: 2
35 },
36 ffmpeg: {
37 threads: 2,
38 nice: 20
39 },
40 registeredInstances: []
41 }
42
43 private id: string
44 private configFilePath: string
45
46 private constructor () {}
47
48 init (id: string) {
49 this.id = id
50 this.configFilePath = join(this.getConfigDir(), 'config.toml')
51 }
52
53 async load () {
54 logger.info(`Using ${this.configFilePath} as configuration file`)
55
56 if (this.isTestInstance()) {
57 logger.info('Removing configuration file as we are using the "test" id')
58 await remove(this.configFilePath)
59 }
60
61 await ensureDir(dirname(this.configFilePath))
62
63 if (!await pathExists(this.configFilePath)) {
64 await this.save()
65 }
66
67 const file = await readFile(this.configFilePath, 'utf-8')
68
69 this.config = merge(this.config, parse(file))
70 }
71
72 save () {
73 return writeFile(this.configFilePath, stringify(this.config))
74 }
75
76 // ---------------------------------------------------------------------------
77
78 async setRegisteredInstances (registeredInstances: {
79 url: string
80 runnerToken: string
81 runnerName: string
82 runnerDescription?: string
83 }[]) {
84 this.config.registeredInstances = registeredInstances
85
86 await this.save()
87 }
88
89 // ---------------------------------------------------------------------------
90
91 getConfig () {
92 return this.deepFreeze(this.config)
93 }
94
95 // ---------------------------------------------------------------------------
96
97 getTranscodingDirectory () {
98 return join(paths.cache, this.id, 'transcoding')
99 }
100
101 getSocketDirectory () {
102 return join(paths.data, this.id)
103 }
104
105 getSocketPath () {
106 return join(this.getSocketDirectory(), 'peertube-runner.sock')
107 }
108
109 getConfigDir () {
110 return join(paths.config, this.id)
111 }
112
113 // ---------------------------------------------------------------------------
114
115 isTestInstance () {
116 return typeof this.id === 'string' && this.id.match(/^test-\d$/)
117 }
118
119 // ---------------------------------------------------------------------------
120
121 // Thanks: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
122 private deepFreeze <T extends object> (object: T) {
123 const propNames = Reflect.ownKeys(object)
124
125 // Freeze properties before freezing self
126 for (const name of propNames) {
127 const value = object[name]
128
129 if ((value && typeof value === 'object') || typeof value === 'function') {
130 this.deepFreeze(value)
131 }
132 }
133
134 return Object.freeze({ ...object })
135 }
136
137 static get Instance () {
138 return this.instance || (this.instance = new this())
139 }
140}
diff --git a/apps/peertube-runner/src/shared/http.ts b/apps/peertube-runner/src/shared/http.ts
new file mode 100644
index 000000000..42886279c
--- /dev/null
+++ b/apps/peertube-runner/src/shared/http.ts
@@ -0,0 +1,67 @@
1import { createWriteStream } from 'fs'
2import { remove } from 'fs-extra/esm'
3import { request as requestHTTP } from 'http'
4import { request as requestHTTPS, RequestOptions } from 'https'
5import { logger } from './logger.js'
6
7export function downloadFile (options: {
8 url: string
9 destination: string
10 runnerToken: string
11 jobToken: string
12}) {
13 const { url, destination, runnerToken, jobToken } = options
14
15 logger.debug(`Downloading file ${url}`)
16
17 return new Promise<void>((res, rej) => {
18 const parsed = new URL(url)
19
20 const body = JSON.stringify({
21 runnerToken,
22 jobToken
23 })
24
25 const getOptions: RequestOptions = {
26 method: 'POST',
27 hostname: parsed.hostname,
28 port: parsed.port,
29 path: parsed.pathname,
30 headers: {
31 'Content-Type': 'application/json',
32 'Content-Length': Buffer.byteLength(body, 'utf-8')
33 }
34 }
35
36 const request = getRequest(url)(getOptions, response => {
37 const code = response.statusCode ?? 0
38
39 if (code >= 400) {
40 return rej(new Error(response.statusMessage))
41 }
42
43 const file = createWriteStream(destination)
44 file.on('finish', () => res())
45
46 response.pipe(file)
47 })
48
49 request.on('error', err => {
50 remove(destination)
51 .catch(err => logger.error(err))
52
53 return rej(err)
54 })
55
56 request.write(body)
57 request.end()
58 })
59}
60
61// ---------------------------------------------------------------------------
62
63function getRequest (url: string) {
64 if (url.startsWith('https://')) return requestHTTPS
65
66 return requestHTTP
67}
diff --git a/apps/peertube-runner/src/shared/index.ts b/apps/peertube-runner/src/shared/index.ts
new file mode 100644
index 000000000..951eef55b
--- /dev/null
+++ b/apps/peertube-runner/src/shared/index.ts
@@ -0,0 +1,3 @@
1export * from './config-manager.js'
2export * from './http.js'
3export * from './logger.js'
diff --git a/apps/peertube-runner/src/shared/ipc/index.ts b/apps/peertube-runner/src/shared/ipc/index.ts
new file mode 100644
index 000000000..337d4de16
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/index.ts
@@ -0,0 +1,2 @@
1export * from './ipc-client.js'
2export * from './ipc-server.js'
diff --git a/apps/peertube-runner/src/shared/ipc/ipc-client.ts b/apps/peertube-runner/src/shared/ipc/ipc-client.ts
new file mode 100644
index 000000000..aa5740dd1
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/ipc-client.ts
@@ -0,0 +1,88 @@
1import CliTable3 from 'cli-table3'
2import { ensureDir } from 'fs-extra/esm'
3import { Client as NetIPC } from 'net-ipc'
4import { ConfigManager } from '../config-manager.js'
5import { IPCReponse, IPCReponseData, IPCRequest } from './shared/index.js'
6
7export class IPCClient {
8 private netIPC: NetIPC
9
10 async run () {
11 await ensureDir(ConfigManager.Instance.getSocketDirectory())
12
13 const socketPath = ConfigManager.Instance.getSocketPath()
14
15 this.netIPC = new NetIPC({ path: socketPath })
16
17 try {
18 await this.netIPC.connect()
19 } catch (err) {
20 if (err.code === 'ECONNREFUSED') {
21 throw new Error(
22 'This runner is not currently running in server mode on this system. ' +
23 'Please run it using the `server` command first (in another terminal for example) and then retry your command.'
24 )
25 }
26
27 throw err
28 }
29 }
30
31 async askRegister (options: {
32 url: string
33 registrationToken: string
34 runnerName: string
35 runnerDescription?: string
36 }) {
37 const req: IPCRequest = {
38 type: 'register',
39 ...options
40 }
41
42 const { success, error } = await this.netIPC.request(req) as IPCReponse
43
44 if (success) console.log('PeerTube instance registered')
45 else console.error('Could not register PeerTube instance on runner server side', error)
46 }
47
48 async askUnregister (options: {
49 url: string
50 runnerName: string
51 }) {
52 const req: IPCRequest = {
53 type: 'unregister',
54 ...options
55 }
56
57 const { success, error } = await this.netIPC.request(req) as IPCReponse
58
59 if (success) console.log('PeerTube instance unregistered')
60 else console.error('Could not unregister PeerTube instance on runner server side', error)
61 }
62
63 async askListRegistered () {
64 const req: IPCRequest = {
65 type: 'list-registered'
66 }
67
68 const { success, error, data } = await this.netIPC.request(req) as IPCReponse<IPCReponseData>
69 if (!success) {
70 console.error('Could not list registered PeerTube instances', error)
71 return
72 }
73
74 const table = new CliTable3({
75 head: [ 'instance', 'runner name', 'runner description' ]
76 })
77
78 for (const server of data.servers) {
79 table.push([ server.url, server.runnerName, server.runnerDescription ])
80 }
81
82 console.log(table.toString())
83 }
84
85 stop () {
86 this.netIPC.destroy()
87 }
88}
diff --git a/apps/peertube-runner/src/shared/ipc/ipc-server.ts b/apps/peertube-runner/src/shared/ipc/ipc-server.ts
new file mode 100644
index 000000000..c68438504
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/ipc-server.ts
@@ -0,0 +1,61 @@
1import { ensureDir } from 'fs-extra/esm'
2import { Server as NetIPC } from 'net-ipc'
3import { pick } from '@peertube/peertube-core-utils'
4import { RunnerServer } from '../../server/index.js'
5import { ConfigManager } from '../config-manager.js'
6import { logger } from '../logger.js'
7import { IPCReponse, IPCReponseData, IPCRequest } from './shared/index.js'
8
9export class IPCServer {
10 private netIPC: NetIPC
11 private runnerServer: RunnerServer
12
13 async run (runnerServer: RunnerServer) {
14 this.runnerServer = runnerServer
15
16 await ensureDir(ConfigManager.Instance.getSocketDirectory())
17
18 const socketPath = ConfigManager.Instance.getSocketPath()
19 this.netIPC = new NetIPC({ path: socketPath })
20 await this.netIPC.start()
21
22 logger.info(`IPC socket created on ${socketPath}`)
23
24 this.netIPC.on('request', async (req: IPCRequest, res) => {
25 try {
26 const data = await this.process(req)
27
28 this.sendReponse(res, { success: true, data })
29 } catch (err) {
30 logger.error('Cannot execute RPC call', err)
31 this.sendReponse(res, { success: false, error: err.message })
32 }
33 })
34 }
35
36 private async process (req: IPCRequest) {
37 switch (req.type) {
38 case 'register':
39 await this.runnerServer.registerRunner(pick(req, [ 'url', 'registrationToken', 'runnerName', 'runnerDescription' ]))
40 return undefined
41
42 case 'unregister':
43 await this.runnerServer.unregisterRunner(pick(req, [ 'url', 'runnerName' ]))
44 return undefined
45
46 case 'list-registered':
47 return Promise.resolve(this.runnerServer.listRegistered())
48
49 default:
50 throw new Error('Unknown RPC call ' + (req as any).type)
51 }
52 }
53
54 private sendReponse <T extends IPCReponseData> (
55 response: (data: any) => Promise<void>,
56 body: IPCReponse<T>
57 ) {
58 response(body)
59 .catch(err => logger.error('Cannot send response after IPC request', err))
60 }
61}
diff --git a/apps/peertube-runner/src/shared/ipc/shared/index.ts b/apps/peertube-runner/src/shared/ipc/shared/index.ts
new file mode 100644
index 000000000..986acafb0
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/shared/index.ts
@@ -0,0 +1,2 @@
1export * from './ipc-request.model.js'
2export * from './ipc-response.model.js'
diff --git a/apps/peertube-runner/src/shared/ipc/shared/ipc-request.model.ts b/apps/peertube-runner/src/shared/ipc/shared/ipc-request.model.ts
new file mode 100644
index 000000000..352808c74
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/shared/ipc-request.model.ts
@@ -0,0 +1,15 @@
1export type IPCRequest =
2 IPCRequestRegister |
3 IPCRequestUnregister |
4 IPCRequestListRegistered
5
6export type IPCRequestRegister = {
7 type: 'register'
8 url: string
9 registrationToken: string
10 runnerName: string
11 runnerDescription?: string
12}
13
14export type IPCRequestUnregister = { type: 'unregister', url: string, runnerName: string }
15export type IPCRequestListRegistered = { type: 'list-registered' }
diff --git a/apps/peertube-runner/src/shared/ipc/shared/ipc-response.model.ts b/apps/peertube-runner/src/shared/ipc/shared/ipc-response.model.ts
new file mode 100644
index 000000000..689d6e09a
--- /dev/null
+++ b/apps/peertube-runner/src/shared/ipc/shared/ipc-response.model.ts
@@ -0,0 +1,15 @@
1export type IPCReponse <T extends IPCReponseData = undefined> = {
2 success: boolean
3 error?: string
4 data?: T
5}
6
7export type IPCReponseData =
8 // list registered
9 {
10 servers: {
11 runnerName: string
12 runnerDescription: string
13 url: string
14 }[]
15 }
diff --git a/apps/peertube-runner/src/shared/logger.ts b/apps/peertube-runner/src/shared/logger.ts
new file mode 100644
index 000000000..ef5283892
--- /dev/null
+++ b/apps/peertube-runner/src/shared/logger.ts
@@ -0,0 +1,12 @@
1import { pino } from 'pino'
2import pretty from 'pino-pretty'
3
4const logger = pino(pretty.default({
5 colorize: true
6}))
7
8logger.level = 'info'
9
10export {
11 logger
12}