aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-04-21 15:05:27 +0200
committerChocobozzz <chocobozzz@cpy.re>2023-05-09 08:57:34 +0200
commit1772b383de490cf406fe93ef3aa3a941f6db513c (patch)
tree7cecc404c8d71951c22079e9bf5180095981b7f9 /packages
parent118626c8752bee7b05c4e0b668852e1aba2416f1 (diff)
downloadPeerTube-1772b383de490cf406fe93ef3aa3a941f6db513c.tar.gz
PeerTube-1772b383de490cf406fe93ef3aa3a941f6db513c.tar.zst
PeerTube-1772b383de490cf406fe93ef3aa3a941f6db513c.zip
Add peertube runner cli
Diffstat (limited to 'packages')
-rw-r--r--packages/peertube-runner/.gitignore2
-rw-r--r--packages/peertube-runner/README.md1
-rw-r--r--packages/peertube-runner/package.json16
-rw-r--r--packages/peertube-runner/peertube-runner.ts84
-rw-r--r--packages/peertube-runner/register/index.ts1
-rw-r--r--packages/peertube-runner/register/register.ts35
-rw-r--r--packages/peertube-runner/server/index.ts1
-rw-r--r--packages/peertube-runner/server/process/index.ts2
-rw-r--r--packages/peertube-runner/server/process/process.ts30
-rw-r--r--packages/peertube-runner/server/process/shared/common.ts91
-rw-r--r--packages/peertube-runner/server/process/shared/index.ts4
-rw-r--r--packages/peertube-runner/server/process/shared/process-live.ts295
-rw-r--r--packages/peertube-runner/server/process/shared/process-vod.ts131
-rw-r--r--packages/peertube-runner/server/process/shared/transcoding-logger.ts10
-rw-r--r--packages/peertube-runner/server/process/shared/transcoding-profiles.ts134
-rw-r--r--packages/peertube-runner/server/server.ts269
-rw-r--r--packages/peertube-runner/shared/config-manager.ts139
-rw-r--r--packages/peertube-runner/shared/http.ts66
-rw-r--r--packages/peertube-runner/shared/index.ts3
-rw-r--r--packages/peertube-runner/shared/ipc/index.ts2
-rw-r--r--packages/peertube-runner/shared/ipc/ipc-client.ts74
-rw-r--r--packages/peertube-runner/shared/ipc/ipc-server.ts61
-rw-r--r--packages/peertube-runner/shared/ipc/shared/index.ts2
-rw-r--r--packages/peertube-runner/shared/ipc/shared/ipc-request.model.ts15
-rw-r--r--packages/peertube-runner/shared/ipc/shared/ipc-response.model.ts15
-rw-r--r--packages/peertube-runner/shared/logger.ts12
-rw-r--r--packages/peertube-runner/tsconfig.json9
-rw-r--r--packages/peertube-runner/yarn.lock528
28 files changed, 2032 insertions, 0 deletions
diff --git a/packages/peertube-runner/.gitignore b/packages/peertube-runner/.gitignore
new file mode 100644
index 000000000..f06235c46
--- /dev/null
+++ b/packages/peertube-runner/.gitignore
@@ -0,0 +1,2 @@
1node_modules
2dist
diff --git a/packages/peertube-runner/README.md b/packages/peertube-runner/README.md
new file mode 100644
index 000000000..b7cf174d5
--- /dev/null
+++ b/packages/peertube-runner/README.md
@@ -0,0 +1 @@
# PeerTube runner
diff --git a/packages/peertube-runner/package.json b/packages/peertube-runner/package.json
new file mode 100644
index 000000000..dde0e2d62
--- /dev/null
+++ b/packages/peertube-runner/package.json
@@ -0,0 +1,16 @@
1{
2 "name": "peertube-runner",
3 "version": "1.0.0",
4 "main": "dist/peertube-runner.js",
5 "license": "AGPL-3.0",
6 "dependencies": {},
7 "devDependencies": {
8 "@commander-js/extra-typings": "^10.0.3",
9 "@iarna/toml": "^2.2.5",
10 "env-paths": "^3.0.0",
11 "esbuild": "^0.17.15",
12 "net-ipc": "^2.0.1",
13 "pino": "^8.11.0",
14 "pino-pretty": "^10.0.0"
15 }
16}
diff --git a/packages/peertube-runner/peertube-runner.ts b/packages/peertube-runner/peertube-runner.ts
new file mode 100644
index 000000000..6bfd9ac0f
--- /dev/null
+++ b/packages/peertube-runner/peertube-runner.ts
@@ -0,0 +1,84 @@
1import { Command, InvalidArgumentError } from '@commander-js/extra-typings'
2import { listRegistered, registerRunner, unregisterRunner } from './register'
3import { RunnerServer } from './server'
4import { ConfigManager, logger } from './shared'
5
6const program = new Command()
7 .option(
8 '--id <id>',
9 'Runner server id, so you can run multiple PeerTube server runners with different configurations on the same machine',
10 'default'
11 )
12 .option('--verbose', 'Run in verbose mode')
13 .hook('preAction', thisCommand => {
14 const options = thisCommand.opts()
15
16 ConfigManager.Instance.init(options.id)
17
18 if (options.verbose === true) {
19 logger.level = 'debug'
20 }
21 })
22
23program.command('server')
24 .description('Run in server mode, to execute remote jobs of registered PeerTube instances')
25 .action(async () => {
26 try {
27 await RunnerServer.Instance.run()
28 } catch (err) {
29 console.error('Cannot run PeerTube runner as server mode', err)
30 process.exit(-1)
31 }
32 })
33
34program.command('register')
35 .description('Register a new PeerTube instance to process runner jobs')
36 .requiredOption('--url <url>', 'PeerTube instance URL', parseUrl)
37 .requiredOption('--registration-token <token>', 'Runner registration token (can be found in PeerTube instance administration')
38 .requiredOption('--runner-name <name>', 'Runner name')
39 .option('--runner-description <description>', 'Runner description')
40 .action(async options => {
41 try {
42 await registerRunner(options)
43 } catch (err) {
44 console.error('Cannot register this PeerTube runner.', err)
45 process.exit(-1)
46 }
47 })
48
49program.command('unregister')
50 .description('Unregister the runner from PeerTube instance')
51 .requiredOption('--url <url>', 'PeerTube instance URL', parseUrl)
52 .action(async options => {
53 try {
54 await unregisterRunner(options)
55 } catch (err) {
56 console.error('Cannot unregister this PeerTube runner.', err)
57 process.exit(-1)
58 }
59 })
60
61program.command('list-registered')
62 .description('List registered PeerTube instances')
63 .action(async () => {
64 try {
65 await listRegistered()
66 } catch (err) {
67 console.error('Cannot list registered PeerTube instances.', err)
68 process.exit(-1)
69 }
70 })
71
72program.parse()
73
74// ---------------------------------------------------------------------------
75// Private
76// ---------------------------------------------------------------------------
77
78function parseUrl (url: string) {
79 if (url.startsWith('http://') !== true && url.startsWith('https://') !== true) {
80 throw new InvalidArgumentError('URL should start with a http:// or https://')
81 }
82
83 return url
84}
diff --git a/packages/peertube-runner/register/index.ts b/packages/peertube-runner/register/index.ts
new file mode 100644
index 000000000..3d4273ef8
--- /dev/null
+++ b/packages/peertube-runner/register/index.ts
@@ -0,0 +1 @@
export * from './register'
diff --git a/packages/peertube-runner/register/register.ts b/packages/peertube-runner/register/register.ts
new file mode 100644
index 000000000..a69390933
--- /dev/null
+++ b/packages/peertube-runner/register/register.ts
@@ -0,0 +1,35 @@
1import { IPCClient } from '../shared/ipc'
2
3export async function registerRunner (options: {
4 url: string
5 registrationToken: string
6 runnerName: string
7 runnerDescription?: string
8}) {
9 const client = new IPCClient()
10 await client.run()
11
12 await client.askRegister(options)
13
14 client.stop()
15}
16
17export async function unregisterRunner (options: {
18 url: string
19}) {
20 const client = new IPCClient()
21 await client.run()
22
23 await client.askUnregister(options)
24
25 client.stop()
26}
27
28export async function listRegistered () {
29 const client = new IPCClient()
30 await client.run()
31
32 await client.askListRegistered()
33
34 client.stop()
35}
diff --git a/packages/peertube-runner/server/index.ts b/packages/peertube-runner/server/index.ts
new file mode 100644
index 000000000..371836515
--- /dev/null
+++ b/packages/peertube-runner/server/index.ts
@@ -0,0 +1 @@
export * from './server'
diff --git a/packages/peertube-runner/server/process/index.ts b/packages/peertube-runner/server/process/index.ts
new file mode 100644
index 000000000..6caedbdaf
--- /dev/null
+++ b/packages/peertube-runner/server/process/index.ts
@@ -0,0 +1,2 @@
1export * from './shared'
2export * from './process'
diff --git a/packages/peertube-runner/server/process/process.ts b/packages/peertube-runner/server/process/process.ts
new file mode 100644
index 000000000..39a929c59
--- /dev/null
+++ b/packages/peertube-runner/server/process/process.ts
@@ -0,0 +1,30 @@
1import { logger } from 'packages/peertube-runner/shared/logger'
2import {
3 RunnerJobLiveRTMPHLSTranscodingPayload,
4 RunnerJobVODAudioMergeTranscodingPayload,
5 RunnerJobVODHLSTranscodingPayload,
6 RunnerJobVODWebVideoTranscodingPayload
7} from '@shared/models'
8import { processAudioMergeTranscoding, processHLSTranscoding, ProcessOptions, processWebVideoTranscoding } from './shared'
9import { ProcessLiveRTMPHLSTranscoding } from './shared/process-live'
10
11export async function processJob (options: ProcessOptions) {
12 const { server, job } = options
13
14 logger.info(`[${server.url}] Processing job of type ${job.type}: ${job.uuid}`, { payload: job.payload })
15
16 if (job.type === 'vod-audio-merge-transcoding') {
17 await processAudioMergeTranscoding(options as ProcessOptions<RunnerJobVODAudioMergeTranscodingPayload>)
18 } else if (job.type === 'vod-web-video-transcoding') {
19 await processWebVideoTranscoding(options as ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>)
20 } else if (job.type === 'vod-hls-transcoding') {
21 await processHLSTranscoding(options as ProcessOptions<RunnerJobVODHLSTranscodingPayload>)
22 } else if (job.type === 'live-rtmp-hls-transcoding') {
23 await new ProcessLiveRTMPHLSTranscoding(options as ProcessOptions<RunnerJobLiveRTMPHLSTranscodingPayload>).process()
24 } else {
25 logger.error(`Unknown job ${job.type} to process`)
26 return
27 }
28
29 logger.info(`[${server.url}] Finished processing job of type ${job.type}: ${job.uuid}`)
30}
diff --git a/packages/peertube-runner/server/process/shared/common.ts b/packages/peertube-runner/server/process/shared/common.ts
new file mode 100644
index 000000000..9b2c40728
--- /dev/null
+++ b/packages/peertube-runner/server/process/shared/common.ts
@@ -0,0 +1,91 @@
1import { throttle } from 'lodash'
2import { ConfigManager, downloadFile, logger } from 'packages/peertube-runner/shared'
3import { join } from 'path'
4import { buildUUID } from '@shared/extra-utils'
5import { FFmpegLive, FFmpegVOD } from '@shared/ffmpeg'
6import { RunnerJob, RunnerJobPayload } from '@shared/models'
7import { PeerTubeServer } from '@shared/server-commands'
8import { getTranscodingLogger } from './transcoding-logger'
9import { getAvailableEncoders, getEncodersToTry } from './transcoding-profiles'
10
11export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
12
13export type ProcessOptions <T extends RunnerJobPayload = RunnerJobPayload> = {
14 server: PeerTubeServer
15 job: JobWithToken<T>
16 runnerToken: string
17}
18
19export async function downloadInputFile (options: {
20 url: string
21 job: JobWithToken
22 runnerToken: string
23}) {
24 const { url, job, runnerToken } = options
25 const destination = join(ConfigManager.Instance.getTranscodingDirectory(), buildUUID())
26
27 await downloadFile({ url, jobToken: job.jobToken, runnerToken, destination })
28
29 return destination
30}
31
32export async function updateTranscodingProgress (options: {
33 server: PeerTubeServer
34 runnerToken: string
35 job: JobWithToken
36 progress: number
37}) {
38 const { server, job, runnerToken, progress } = options
39
40 return server.runnerJobs.update({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken, progress })
41}
42
43export function buildFFmpegVOD (options: {
44 server: PeerTubeServer
45 runnerToken: string
46 job: JobWithToken
47}) {
48 const { server, job, runnerToken } = options
49
50 const updateInterval = ConfigManager.Instance.isTestInstance()
51 ? 500
52 : 60000
53
54 const updateJobProgress = throttle((progress: number) => {
55 if (progress < 0 || progress > 100) progress = undefined
56
57 updateTranscodingProgress({ server, job, runnerToken, progress })
58 .catch(err => logger.error({ err }, 'Cannot send job progress'))
59 }, updateInterval, { trailing: false })
60
61 const config = ConfigManager.Instance.getConfig()
62
63 return new FFmpegVOD({
64 niceness: config.ffmpeg.nice,
65 threads: config.ffmpeg.threads,
66 tmpDirectory: ConfigManager.Instance.getTranscodingDirectory(),
67 profile: 'default',
68 availableEncoders: {
69 available: getAvailableEncoders(),
70 encodersToTry: getEncodersToTry()
71 },
72 logger: getTranscodingLogger(),
73 updateJobProgress
74 })
75}
76
77export function buildFFmpegLive () {
78 const config = ConfigManager.Instance.getConfig()
79
80 return new FFmpegLive({
81 niceness: config.ffmpeg.nice,
82 threads: config.ffmpeg.threads,
83 tmpDirectory: ConfigManager.Instance.getTranscodingDirectory(),
84 profile: 'default',
85 availableEncoders: {
86 available: getAvailableEncoders(),
87 encodersToTry: getEncodersToTry()
88 },
89 logger: getTranscodingLogger()
90 })
91}
diff --git a/packages/peertube-runner/server/process/shared/index.ts b/packages/peertube-runner/server/process/shared/index.ts
new file mode 100644
index 000000000..8e09a7869
--- /dev/null
+++ b/packages/peertube-runner/server/process/shared/index.ts
@@ -0,0 +1,4 @@
1export * from './common'
2export * from './process-vod'
3export * from './transcoding-logger'
4export * from './transcoding-profiles'
diff --git a/packages/peertube-runner/server/process/shared/process-live.ts b/packages/peertube-runner/server/process/shared/process-live.ts
new file mode 100644
index 000000000..5a3b596a2
--- /dev/null
+++ b/packages/peertube-runner/server/process/shared/process-live.ts
@@ -0,0 +1,295 @@
1import { FSWatcher, watch } from 'chokidar'
2import { FfmpegCommand } from 'fluent-ffmpeg'
3import { ensureDir, remove } from 'fs-extra'
4import { logger } from 'packages/peertube-runner/shared'
5import { basename, join } from 'path'
6import { wait } from '@shared/core-utils'
7import { buildUUID } from '@shared/extra-utils'
8import { ffprobePromise, getVideoStreamBitrate, getVideoStreamDimensionsInfo, hasAudioStream } from '@shared/ffmpeg'
9import {
10 LiveRTMPHLSTranscodingSuccess,
11 LiveRTMPHLSTranscodingUpdatePayload,
12 PeerTubeProblemDocument,
13 RunnerJobLiveRTMPHLSTranscodingPayload,
14 ServerErrorCode
15} from '@shared/models'
16import { ConfigManager } from '../../../shared/config-manager'
17import { buildFFmpegLive, ProcessOptions } from './common'
18
19export class ProcessLiveRTMPHLSTranscoding {
20
21 private readonly outputPath: string
22 private readonly fsWatchers: FSWatcher[] = []
23
24 private readonly playlistsCreated = new Set<string>()
25 private allPlaylistsCreated = false
26
27 private ffmpegCommand: FfmpegCommand
28
29 private ended = false
30 private errored = false
31
32 constructor (private readonly options: ProcessOptions<RunnerJobLiveRTMPHLSTranscodingPayload>) {
33 this.outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), buildUUID())
34 }
35
36 process () {
37 const job = this.options.job
38 const payload = job.payload
39
40 return new Promise<void>(async (res, rej) => {
41 try {
42 await ensureDir(this.outputPath)
43
44 logger.info(`Probing ${payload.input.rtmpUrl}`)
45 const probe = await ffprobePromise(payload.input.rtmpUrl)
46 logger.info({ probe }, `Probed ${payload.input.rtmpUrl}`)
47
48 const hasAudio = await hasAudioStream(payload.input.rtmpUrl, probe)
49 const bitrate = await getVideoStreamBitrate(payload.input.rtmpUrl, probe)
50 const { ratio } = await getVideoStreamDimensionsInfo(payload.input.rtmpUrl, probe)
51
52 const m3u8Watcher = watch(this.outputPath + '/*.m3u8')
53 this.fsWatchers.push(m3u8Watcher)
54
55 const tsWatcher = watch(this.outputPath + '/*.ts')
56 this.fsWatchers.push(tsWatcher)
57
58 m3u8Watcher.on('change', p => {
59 logger.debug(`${p} m3u8 playlist changed`)
60 })
61
62 m3u8Watcher.on('add', p => {
63 this.playlistsCreated.add(p)
64
65 if (this.playlistsCreated.size === this.options.job.payload.output.toTranscode.length + 1) {
66 this.allPlaylistsCreated = true
67 logger.info('All m3u8 playlists are created.')
68 }
69 })
70
71 tsWatcher.on('add', p => {
72 this.sendAddedChunkUpdate(p)
73 .catch(err => this.onUpdateError(err, rej))
74 })
75
76 tsWatcher.on('unlink', p => {
77 this.sendDeletedChunkUpdate(p)
78 .catch(err => this.onUpdateError(err, rej))
79 })
80
81 this.ffmpegCommand = await buildFFmpegLive().getLiveTranscodingCommand({
82 inputUrl: payload.input.rtmpUrl,
83
84 outPath: this.outputPath,
85 masterPlaylistName: 'master.m3u8',
86
87 segmentListSize: payload.output.segmentListSize,
88 segmentDuration: payload.output.segmentDuration,
89
90 toTranscode: payload.output.toTranscode,
91
92 bitrate,
93 ratio,
94
95 hasAudio
96 })
97
98 logger.info(`Running live transcoding for ${payload.input.rtmpUrl}`)
99
100 this.ffmpegCommand.on('error', (err, stdout, stderr) => {
101 this.onFFmpegError({ err, stdout, stderr })
102
103 res()
104 })
105
106 this.ffmpegCommand.on('end', () => {
107 this.onFFmpegEnded()
108 .catch(err => logger.error({ err }, 'Error in FFmpeg end handler'))
109
110 res()
111 })
112
113 this.ffmpegCommand.run()
114 } catch (err) {
115 rej(err)
116 }
117 })
118 }
119
120 // ---------------------------------------------------------------------------
121
122 private onUpdateError (err: Error, reject: (reason?: any) => void) {
123 if (this.errored) return
124 if (this.ended) return
125
126 this.errored = true
127
128 reject(err)
129 this.ffmpegCommand.kill('SIGINT')
130
131 const type = ((err as any).res?.body as PeerTubeProblemDocument)?.code
132 if (type === ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE) {
133 logger.info({ err }, 'Stopping transcoding as the job is not in processing state anymore')
134 } else {
135 logger.error({ err }, 'Cannot send update after added/deleted chunk, stopping live transcoding')
136
137 this.sendError(err)
138 .catch(subErr => logger.error({ err: subErr }, 'Cannot send error'))
139 }
140
141 this.cleanup()
142 }
143
144 // ---------------------------------------------------------------------------
145
146 private onFFmpegError (options: {
147 err: any
148 stdout: string
149 stderr: string
150 }) {
151 const { err, stdout, stderr } = options
152
153 // Don't care that we killed the ffmpeg process
154 if (err?.message?.includes('Exiting normally')) return
155 if (this.errored) return
156 if (this.ended) return
157
158 this.errored = true
159
160 logger.error({ err, stdout, stderr }, 'FFmpeg transcoding error.')
161
162 this.sendError(err)
163 .catch(subErr => logger.error({ err: subErr }, 'Cannot send error'))
164
165 this.cleanup()
166 }
167
168 private async sendError (err: Error) {
169 await this.options.server.runnerJobs.error({
170 jobToken: this.options.job.jobToken,
171 jobUUID: this.options.job.uuid,
172 runnerToken: this.options.runnerToken,
173 message: err.message
174 })
175 }
176
177 // ---------------------------------------------------------------------------
178
179 private async onFFmpegEnded () {
180 if (this.ended) return
181
182 this.ended = true
183 logger.info('FFmpeg ended, sending success to server')
184
185 // Wait last ffmpeg chunks generation
186 await wait(1500)
187
188 this.sendSuccess()
189 .catch(err => logger.error({ err }, 'Cannot send success'))
190
191 this.cleanup()
192 }
193
194 private async sendSuccess () {
195 const successBody: LiveRTMPHLSTranscodingSuccess = {}
196
197 await this.options.server.runnerJobs.success({
198 jobToken: this.options.job.jobToken,
199 jobUUID: this.options.job.uuid,
200 runnerToken: this.options.runnerToken,
201 payload: successBody
202 })
203 }
204
205 // ---------------------------------------------------------------------------
206
207 private sendDeletedChunkUpdate (deletedChunk: string) {
208 if (this.ended) return
209
210 logger.debug(`Sending removed live chunk ${deletedChunk} update`)
211
212 const videoChunkFilename = basename(deletedChunk)
213
214 let payload: LiveRTMPHLSTranscodingUpdatePayload = {
215 type: 'remove-chunk',
216 videoChunkFilename
217 }
218
219 if (this.allPlaylistsCreated) {
220 const playlistName = this.getPlaylistName(videoChunkFilename)
221
222 payload = {
223 ...payload,
224 masterPlaylistFile: join(this.outputPath, 'master.m3u8'),
225 resolutionPlaylistFilename: playlistName,
226 resolutionPlaylistFile: join(this.outputPath, playlistName)
227 }
228 }
229
230 return this.updateWithRetry(payload)
231 }
232
233 private sendAddedChunkUpdate (addedChunk: string) {
234 if (this.ended) return
235
236 logger.debug(`Sending added live chunk ${addedChunk} update`)
237
238 const videoChunkFilename = basename(addedChunk)
239
240 let payload: LiveRTMPHLSTranscodingUpdatePayload = {
241 type: 'add-chunk',
242 videoChunkFilename,
243 videoChunkFile: addedChunk
244 }
245
246 if (this.allPlaylistsCreated) {
247 const playlistName = this.getPlaylistName(videoChunkFilename)
248
249 payload = {
250 ...payload,
251 masterPlaylistFile: join(this.outputPath, 'master.m3u8'),
252 resolutionPlaylistFilename: playlistName,
253 resolutionPlaylistFile: join(this.outputPath, playlistName)
254 }
255 }
256
257 return this.updateWithRetry(payload)
258 }
259
260 private async updateWithRetry (payload: LiveRTMPHLSTranscodingUpdatePayload, currentTry = 1) {
261 if (this.ended || this.errored) return
262
263 try {
264 await this.options.server.runnerJobs.update({
265 jobToken: this.options.job.jobToken,
266 jobUUID: this.options.job.uuid,
267 runnerToken: this.options.runnerToken,
268 payload
269 })
270 } catch (err) {
271 if (currentTry >= 3) throw err
272
273 logger.warn({ err }, 'Will retry update after error')
274 await wait(250)
275
276 return this.updateWithRetry(payload, currentTry + 1)
277 }
278 }
279
280 private getPlaylistName (videoChunkFilename: string) {
281 return `${videoChunkFilename.split('-')[0]}.m3u8`
282 }
283
284 // ---------------------------------------------------------------------------
285
286 private cleanup () {
287 for (const fsWatcher of this.fsWatchers) {
288 fsWatcher.close()
289 .catch(err => logger.error({ err }, 'Cannot close watcher'))
290 }
291
292 remove(this.outputPath)
293 .catch(err => logger.error({ err }, `Cannot remove ${this.outputPath}`))
294 }
295}
diff --git a/packages/peertube-runner/server/process/shared/process-vod.ts b/packages/peertube-runner/server/process/shared/process-vod.ts
new file mode 100644
index 000000000..aae61e9c5
--- /dev/null
+++ b/packages/peertube-runner/server/process/shared/process-vod.ts
@@ -0,0 +1,131 @@
1import { remove } from 'fs-extra'
2import { join } from 'path'
3import { buildUUID } from '@shared/extra-utils'
4import {
5 RunnerJobVODAudioMergeTranscodingPayload,
6 RunnerJobVODHLSTranscodingPayload,
7 RunnerJobVODWebVideoTranscodingPayload,
8 VODAudioMergeTranscodingSuccess,
9 VODHLSTranscodingSuccess,
10 VODWebVideoTranscodingSuccess
11} from '@shared/models'
12import { ConfigManager } from '../../../shared/config-manager'
13import { buildFFmpegVOD, downloadInputFile, ProcessOptions } from './common'
14
15export async function processWebVideoTranscoding (options: ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>) {
16 const { server, job, runnerToken } = options
17 const payload = job.payload
18
19 const inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
20
21 const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken })
22
23 const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`)
24
25 await ffmpegVod.transcode({
26 type: 'video',
27
28 inputPath,
29
30 outputPath,
31
32 inputFileMutexReleaser: () => {},
33
34 resolution: payload.output.resolution,
35 fps: payload.output.fps
36 })
37
38 const successBody: VODWebVideoTranscodingSuccess = {
39 videoFile: outputPath
40 }
41
42 await server.runnerJobs.success({
43 jobToken: job.jobToken,
44 jobUUID: job.uuid,
45 runnerToken,
46 payload: successBody
47 })
48
49 await remove(outputPath)
50}
51
52export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVODHLSTranscodingPayload>) {
53 const { server, job, runnerToken } = options
54 const payload = job.payload
55
56 const inputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
57 const uuid = buildUUID()
58
59 const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `${uuid}-${payload.output.resolution}.m3u8`)
60 const videoFilename = `${uuid}-${payload.output.resolution}-fragmented.mp4`
61 const videoPath = join(join(ConfigManager.Instance.getTranscodingDirectory(), videoFilename))
62
63 const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken })
64
65 await ffmpegVod.transcode({
66 type: 'hls',
67 copyCodecs: false,
68 inputPath,
69 hlsPlaylist: { videoFilename },
70 outputPath,
71
72 inputFileMutexReleaser: () => {},
73
74 resolution: payload.output.resolution,
75 fps: payload.output.fps
76 })
77
78 const successBody: VODHLSTranscodingSuccess = {
79 resolutionPlaylistFile: outputPath,
80 videoFile: videoPath
81 }
82
83 await server.runnerJobs.success({
84 jobToken: job.jobToken,
85 jobUUID: job.uuid,
86 runnerToken,
87 payload: successBody
88 })
89
90 await remove(outputPath)
91 await remove(videoPath)
92}
93
94export async function processAudioMergeTranscoding (options: ProcessOptions<RunnerJobVODAudioMergeTranscodingPayload>) {
95 const { server, job, runnerToken } = options
96 const payload = job.payload
97
98 const audioPath = await downloadInputFile({ url: payload.input.audioFileUrl, runnerToken, job })
99 const inputPath = await downloadInputFile({ url: payload.input.previewFileUrl, runnerToken, job })
100
101 const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`)
102
103 const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken })
104
105 await ffmpegVod.transcode({
106 type: 'merge-audio',
107
108 audioPath,
109 inputPath,
110
111 outputPath,
112
113 inputFileMutexReleaser: () => {},
114
115 resolution: payload.output.resolution,
116 fps: payload.output.fps
117 })
118
119 const successBody: VODAudioMergeTranscodingSuccess = {
120 videoFile: outputPath
121 }
122
123 await server.runnerJobs.success({
124 jobToken: job.jobToken,
125 jobUUID: job.uuid,
126 runnerToken,
127 payload: successBody
128 })
129
130 await remove(outputPath)
131}
diff --git a/packages/peertube-runner/server/process/shared/transcoding-logger.ts b/packages/peertube-runner/server/process/shared/transcoding-logger.ts
new file mode 100644
index 000000000..d0f928914
--- /dev/null
+++ b/packages/peertube-runner/server/process/shared/transcoding-logger.ts
@@ -0,0 +1,10 @@
1import { logger } from 'packages/peertube-runner/shared/logger'
2
3export function getTranscodingLogger () {
4 return {
5 info: logger.info.bind(logger),
6 debug: logger.debug.bind(logger),
7 warn: logger.warn.bind(logger),
8 error: logger.error.bind(logger)
9 }
10}
diff --git a/packages/peertube-runner/server/process/shared/transcoding-profiles.ts b/packages/peertube-runner/server/process/shared/transcoding-profiles.ts
new file mode 100644
index 000000000..492d17d6a
--- /dev/null
+++ b/packages/peertube-runner/server/process/shared/transcoding-profiles.ts
@@ -0,0 +1,134 @@
1import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils'
2import { buildStreamSuffix, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '@shared/ffmpeg'
3import { EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '@shared/models'
4
5const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => {
6 const { fps, inputRatio, inputBitrate, resolution } = options
7
8 const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution })
9
10 return {
11 outputOptions: [
12 ...getCommonOutputOptions(targetBitrate),
13
14 `-r ${fps}`
15 ]
16 }
17}
18
19const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => {
20 const { streamNum, fps, inputBitrate, inputRatio, resolution } = options
21
22 const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution })
23
24 return {
25 outputOptions: [
26 ...getCommonOutputOptions(targetBitrate, streamNum),
27
28 `${buildStreamSuffix('-r:v', streamNum)} ${fps}`,
29 `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`
30 ]
31 }
32}
33
34const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum, canCopyAudio }) => {
35 const probe = await ffprobePromise(input)
36
37 const parsedAudio = await getAudioStream(input, probe)
38
39 // We try to reduce the ceiling bitrate by making rough matches of bitrates
40 // Of course this is far from perfect, but it might save some space in the end
41
42 const audioCodecName = parsedAudio.audioStream['codec_name']
43
44 const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate)
45
46 // Force stereo as it causes some issues with HLS playback in Chrome
47 const base = [ '-channel_layout', 'stereo' ]
48
49 if (bitrate !== -1) {
50 return { outputOptions: base.concat([ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ]) }
51 }
52
53 return { outputOptions: base }
54}
55
56const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => {
57 return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
58}
59
60export function getAvailableEncoders () {
61 return {
62 vod: {
63 libx264: {
64 default: defaultX264VODOptionsBuilder
65 },
66 aac: {
67 default: defaultAACOptionsBuilder
68 },
69 libfdk_aac: {
70 default: defaultLibFDKAACVODOptionsBuilder
71 }
72 },
73 live: {
74 libx264: {
75 default: defaultX264LiveOptionsBuilder
76 },
77 aac: {
78 default: defaultAACOptionsBuilder
79 }
80 }
81 }
82}
83
84export function getEncodersToTry () {
85 return {
86 vod: {
87 video: [ 'libx264' ],
88 audio: [ 'libfdk_aac', 'aac' ]
89 },
90
91 live: {
92 video: [ 'libx264' ],
93 audio: [ 'libfdk_aac', 'aac' ]
94 }
95 }
96}
97
98// ---------------------------------------------------------------------------
99
100function getTargetBitrate (options: {
101 inputBitrate: number
102 resolution: VideoResolution
103 ratio: number
104 fps: number
105}) {
106 const { inputBitrate, resolution, ratio, fps } = options
107
108 const capped = capBitrate(inputBitrate, getAverageBitrate({ resolution, fps, ratio }))
109 const limit = getMinLimitBitrate({ resolution, fps, ratio })
110
111 return Math.max(limit, capped)
112}
113
114function capBitrate (inputBitrate: number, targetBitrate: number) {
115 if (!inputBitrate) return targetBitrate
116
117 // Add 30% margin to input bitrate
118 const inputBitrateWithMargin = inputBitrate + (inputBitrate * 0.3)
119
120 return Math.min(targetBitrate, inputBitrateWithMargin)
121}
122
123function getCommonOutputOptions (targetBitrate: number, streamNum?: number) {
124 return [
125 `-preset veryfast`,
126 `${buildStreamSuffix('-maxrate:v', streamNum)} ${targetBitrate}`,
127 `${buildStreamSuffix('-bufsize:v', streamNum)} ${targetBitrate * 2}`,
128
129 // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
130 `-b_strategy 1`,
131 // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
132 `-bf 16`
133 ]
134}
diff --git a/packages/peertube-runner/server/server.ts b/packages/peertube-runner/server/server.ts
new file mode 100644
index 000000000..724f359bd
--- /dev/null
+++ b/packages/peertube-runner/server/server.ts
@@ -0,0 +1,269 @@
1import { ensureDir, readdir, remove } from 'fs-extra'
2import { join } from 'path'
3import { io, Socket } from 'socket.io-client'
4import { pick } from '@shared/core-utils'
5import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models'
6import { PeerTubeServer as PeerTubeServerCommand } from '@shared/server-commands'
7import { ConfigManager } from '../shared'
8import { IPCServer } from '../shared/ipc'
9import { logger } from '../shared/logger'
10import { JobWithToken, processJob } from './process'
11
12type PeerTubeServer = PeerTubeServerCommand & {
13 runnerToken: string
14 runnerName: string
15 runnerDescription?: string
16}
17
18export class RunnerServer {
19 private static instance: RunnerServer
20
21 private servers: PeerTubeServer[] = []
22 private processingJobs: { job: JobWithToken, server: PeerTubeServer }[] = []
23
24 private checkingAvailableJobs = false
25
26 private readonly sockets = new Map<PeerTubeServer, Socket>()
27
28 private constructor () {}
29
30 async run () {
31 logger.info('Running PeerTube runner in server mode')
32
33 await ConfigManager.Instance.load()
34
35 for (const registered of ConfigManager.Instance.getConfig().registeredInstances) {
36 const serverCommand = new PeerTubeServerCommand({ url: registered.url })
37
38 this.loadServer(Object.assign(serverCommand, registered))
39
40 logger.info(`Loading registered instance ${registered.url}`)
41 }
42
43 // Run IPC
44 const ipcServer = new IPCServer()
45 try {
46 await ipcServer.run(this)
47 } catch (err) {
48 console.error('Cannot start local socket for IPC communication', err)
49 process.exit(-1)
50 }
51
52 // Cleanup on exit
53 for (const code of [ 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException' ]) {
54 process.on(code, async () => {
55 await this.onExit()
56 })
57 }
58
59 // Process jobs
60 await ensureDir(ConfigManager.Instance.getTranscodingDirectory())
61 await this.cleanupTMP()
62
63 logger.info(`Using ${ConfigManager.Instance.getTranscodingDirectory()} for transcoding directory`)
64
65 await this.checkAvailableJobs()
66 }
67
68 // ---------------------------------------------------------------------------
69
70 async registerRunner (options: {
71 url: string
72 registrationToken: string
73 runnerName: string
74 runnerDescription?: string
75 }) {
76 const { url, registrationToken, runnerName, runnerDescription } = options
77
78 logger.info(`Registering runner ${runnerName} on ${url}...`)
79
80 const serverCommand = new PeerTubeServerCommand({ url })
81 const { runnerToken } = await serverCommand.runners.register({ name: runnerName, description: runnerDescription, registrationToken })
82
83 const server: PeerTubeServer = Object.assign(serverCommand, {
84 runnerToken,
85 runnerName,
86 runnerDescription
87 })
88
89 this.loadServer(server)
90 await this.saveRegisteredInstancesInConf()
91
92 logger.info(`Registered runner ${runnerName} on ${url}`)
93
94 await this.checkAvailableJobs()
95 }
96
97 private loadServer (server: PeerTubeServer) {
98 this.servers.push(server)
99
100 const url = server.url + '/runners'
101 const socket = io(url, {
102 auth: {
103 runnerToken: server.runnerToken
104 },
105 transports: [ 'websocket' ]
106 })
107
108 socket.on('connect_error', err => logger.warn({ err }, `Cannot connect to ${url} socket`))
109 socket.on('connect', () => logger.info(`Connected to ${url} socket`))
110 socket.on('available-jobs', () => this.checkAvailableJobs())
111
112 this.sockets.set(server, socket)
113 }
114
115 async unregisterRunner (options: {
116 url: string
117 }) {
118 const { url } = options
119
120 const server = this.servers.find(s => s.url === url)
121 if (!server) {
122 logger.error(`Unknown server ${url} to unregister`)
123 return
124 }
125
126 logger.info(`Unregistering runner ${server.runnerName} on ${url}...`)
127
128 try {
129 await server.runners.unregister({ runnerToken: server.runnerToken })
130 } catch (err) {
131 logger.error({ err }, `Cannot unregister runner ${server.runnerName} on ${url}`)
132 }
133
134 this.unloadServer(server)
135 await this.saveRegisteredInstancesInConf()
136
137 logger.info(`Unregistered runner ${server.runnerName} on ${server.url}`)
138 }
139
140 private unloadServer (server: PeerTubeServer) {
141 this.servers = this.servers.filter(s => s !== server)
142
143 const socket = this.sockets.get(server)
144 socket.disconnect()
145
146 this.sockets.delete(server)
147 }
148
149 listRegistered () {
150 return {
151 servers: this.servers.map(s => {
152 return {
153 url: s.url,
154 runnerName: s.runnerName,
155 runnerDescription: s.runnerDescription
156 }
157 })
158 }
159 }
160
161 // ---------------------------------------------------------------------------
162
163 private async checkAvailableJobs () {
164 if (this.checkingAvailableJobs) return
165
166 logger.info('Checking available jobs')
167
168 this.checkingAvailableJobs = true
169
170 for (const server of this.servers) {
171 try {
172 const job = await this.requestJob(server)
173 if (!job) continue
174
175 await this.tryToExecuteJobAsync(server, job)
176 } catch (err) {
177 if ((err.res?.body as PeerTubeProblemDocument)?.code === ServerErrorCode.UNKNOWN_RUNNER_TOKEN) {
178 logger.error({ err }, `Unregistering ${server.url} as the runner token ${server.runnerToken} is invalid`)
179
180 await this.unregisterRunner({ url: server.url })
181 return
182 }
183
184 logger.error({ err }, `Cannot request/accept job on ${server.url} for runner ${server.runnerName}`)
185 }
186 }
187
188 this.checkingAvailableJobs = false
189 }
190
191 private async requestJob (server: PeerTubeServer) {
192 logger.debug(`Requesting jobs on ${server.url} for runner ${server.runnerName}`)
193
194 const { availableJobs } = await server.runnerJobs.request({ runnerToken: server.runnerToken })
195
196 if (availableJobs.length === 0) {
197 logger.debug(`No job available on ${server.url} for runner ${server.runnerName}`)
198 return undefined
199 }
200
201 return availableJobs[0]
202 }
203
204 private async tryToExecuteJobAsync (server: PeerTubeServer, jobToAccept: { uuid: string }) {
205 if (this.processingJobs.length >= ConfigManager.Instance.getConfig().jobs.concurrency) return
206
207 const { job } = await server.runnerJobs.accept({ runnerToken: server.runnerToken, jobUUID: jobToAccept.uuid })
208
209 const processingJob = { job, server }
210 this.processingJobs.push(processingJob)
211
212 processJob({ server, job, runnerToken: server.runnerToken })
213 .catch(err => {
214 logger.error({ err }, 'Cannot process job')
215
216 server.runnerJobs.error({ jobToken: job.jobToken, jobUUID: job.uuid, runnerToken: server.runnerToken, message: err.message })
217 .catch(err2 => logger.error({ err: err2 }, 'Cannot abort job after error'))
218 })
219 .finally(() => {
220 this.processingJobs = this.processingJobs.filter(p => p !== processingJob)
221
222 return this.checkAvailableJobs()
223 })
224 }
225
226 // ---------------------------------------------------------------------------
227
228 private saveRegisteredInstancesInConf () {
229 const data = this.servers.map(s => {
230 return pick(s, [ 'url', 'runnerToken', 'runnerName', 'runnerDescription' ])
231 })
232
233 return ConfigManager.Instance.setRegisteredInstances(data)
234 }
235
236 // ---------------------------------------------------------------------------
237
238 private async cleanupTMP () {
239 const files = await readdir(ConfigManager.Instance.getTranscodingDirectory())
240
241 for (const file of files) {
242 await remove(join(ConfigManager.Instance.getTranscodingDirectory(), file))
243 }
244 }
245
246 private async onExit () {
247 try {
248 for (const { server, job } of this.processingJobs) {
249 await server.runnerJobs.abort({
250 jobToken: job.jobToken,
251 jobUUID: job.uuid,
252 reason: 'Runner stopped',
253 runnerToken: server.runnerToken
254 })
255 }
256
257 await this.cleanupTMP()
258 } catch (err) {
259 console.error(err)
260 process.exit(-1)
261 }
262
263 process.exit()
264 }
265
266 static get Instance () {
267 return this.instance || (this.instance = new this())
268 }
269}
diff --git a/packages/peertube-runner/shared/config-manager.ts b/packages/peertube-runner/shared/config-manager.ts
new file mode 100644
index 000000000..352bae1fa
--- /dev/null
+++ b/packages/peertube-runner/shared/config-manager.ts
@@ -0,0 +1,139 @@
1import envPaths from 'env-paths'
2import { ensureDir, pathExists, readFile, remove, writeFile } from 'fs-extra'
3import { merge } from 'lodash'
4import { logger } from 'packages/peertube-runner/shared/logger'
5import { dirname, join } from 'path'
6import { parse, stringify } from '@iarna/toml'
7
8const paths = envPaths('peertube-runner')
9
10type Config = {
11 jobs: {
12 concurrency: number
13 }
14
15 ffmpeg: {
16 threads: number
17 nice: number
18 }
19
20 registeredInstances: {
21 url: string
22 runnerToken: string
23 runnerName: string
24 runnerDescription?: string
25 }[]
26}
27
28export class ConfigManager {
29 private static instance: ConfigManager
30
31 private config: Config = {
32 jobs: {
33 concurrency: 2
34 },
35 ffmpeg: {
36 threads: 2,
37 nice: 20
38 },
39 registeredInstances: []
40 }
41
42 private id: string
43 private configFilePath: string
44
45 private constructor () {}
46
47 init (id: string) {
48 this.id = id
49 this.configFilePath = join(this.getConfigDir(), 'config.toml')
50 }
51
52 async load () {
53 logger.info(`Using ${this.configFilePath} as configuration file`)
54
55 if (this.isTestInstance()) {
56 logger.info('Removing configuration file as we are using the "test" id')
57 await remove(this.configFilePath)
58 }
59
60 await ensureDir(dirname(this.configFilePath))
61
62 if (!await pathExists(this.configFilePath)) {
63 await this.save()
64 }
65
66 const file = await readFile(this.configFilePath, 'utf-8')
67
68 this.config = merge(this.config, parse(file))
69 }
70
71 save () {
72 return writeFile(this.configFilePath, stringify(this.config))
73 }
74
75 // ---------------------------------------------------------------------------
76
77 async setRegisteredInstances (registeredInstances: {
78 url: string
79 runnerToken: string
80 runnerName: string
81 runnerDescription?: string
82 }[]) {
83 this.config.registeredInstances = registeredInstances
84
85 await this.save()
86 }
87
88 // ---------------------------------------------------------------------------
89
90 getConfig () {
91 return this.deepFreeze(this.config)
92 }
93
94 // ---------------------------------------------------------------------------
95
96 getTranscodingDirectory () {
97 return join(paths.cache, this.id, 'transcoding')
98 }
99
100 getSocketDirectory () {
101 return join(paths.data, this.id)
102 }
103
104 getSocketPath () {
105 return join(this.getSocketDirectory(), 'peertube-runner.sock')
106 }
107
108 getConfigDir () {
109 return join(paths.config, this.id)
110 }
111
112 // ---------------------------------------------------------------------------
113
114 isTestInstance () {
115 return this.id === 'test'
116 }
117
118 // ---------------------------------------------------------------------------
119
120 // Thanks: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
121 private deepFreeze <T extends object> (object: T) {
122 const propNames = Reflect.ownKeys(object)
123
124 // Freeze properties before freezing self
125 for (const name of propNames) {
126 const value = object[name]
127
128 if ((value && typeof value === 'object') || typeof value === 'function') {
129 this.deepFreeze(value)
130 }
131 }
132
133 return Object.freeze({ ...object })
134 }
135
136 static get Instance () {
137 return this.instance || (this.instance = new this())
138 }
139}
diff --git a/packages/peertube-runner/shared/http.ts b/packages/peertube-runner/shared/http.ts
new file mode 100644
index 000000000..d3fff70d1
--- /dev/null
+++ b/packages/peertube-runner/shared/http.ts
@@ -0,0 +1,66 @@
1import { createWriteStream, remove } from 'fs-extra'
2import { request as requestHTTP } from 'http'
3import { request as requestHTTPS, RequestOptions } from 'https'
4import { logger } from './logger'
5
6export function downloadFile (options: {
7 url: string
8 destination: string
9 runnerToken: string
10 jobToken: string
11}) {
12 const { url, destination, runnerToken, jobToken } = options
13
14 logger.debug(`Downloading file ${url}`)
15
16 return new Promise<void>((res, rej) => {
17 const parsed = new URL(url)
18
19 const body = JSON.stringify({
20 runnerToken,
21 jobToken
22 })
23
24 const getOptions: RequestOptions = {
25 method: 'POST',
26 hostname: parsed.hostname,
27 port: parsed.port,
28 path: parsed.pathname,
29 headers: {
30 'Content-Type': 'application/json',
31 'Content-Length': Buffer.byteLength(body, 'utf-8')
32 }
33 }
34
35 const request = getRequest(url)(getOptions, response => {
36 const code = response.statusCode ?? 0
37
38 if (code >= 400) {
39 return rej(new Error(response.statusMessage))
40 }
41
42 const file = createWriteStream(destination)
43 file.on('finish', () => res())
44
45 response.pipe(file)
46 })
47
48 request.on('error', err => {
49 remove(destination)
50 .catch(err => console.error(err))
51
52 return rej(err)
53 })
54
55 request.write(body)
56 request.end()
57 })
58}
59
60// ---------------------------------------------------------------------------
61
62function getRequest (url: string) {
63 if (url.startsWith('https://')) return requestHTTPS
64
65 return requestHTTP
66}
diff --git a/packages/peertube-runner/shared/index.ts b/packages/peertube-runner/shared/index.ts
new file mode 100644
index 000000000..d0b5a2e3e
--- /dev/null
+++ b/packages/peertube-runner/shared/index.ts
@@ -0,0 +1,3 @@
1export * from './config-manager'
2export * from './http'
3export * from './logger'
diff --git a/packages/peertube-runner/shared/ipc/index.ts b/packages/peertube-runner/shared/ipc/index.ts
new file mode 100644
index 000000000..ad4590281
--- /dev/null
+++ b/packages/peertube-runner/shared/ipc/index.ts
@@ -0,0 +1,2 @@
1export * from './ipc-client'
2export * from './ipc-server'
diff --git a/packages/peertube-runner/shared/ipc/ipc-client.ts b/packages/peertube-runner/shared/ipc/ipc-client.ts
new file mode 100644
index 000000000..7f5951157
--- /dev/null
+++ b/packages/peertube-runner/shared/ipc/ipc-client.ts
@@ -0,0 +1,74 @@
1import CliTable3 from 'cli-table3'
2import { ensureDir } from 'fs-extra'
3import { Client as NetIPC } from 'net-ipc'
4import { ConfigManager } from '../config-manager'
5import { IPCReponse, IPCReponseData, IPCRequest } from './shared'
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 this.netIPC = new NetIPC({ path: socketPath })
15 await this.netIPC.connect()
16 }
17
18 async askRegister (options: {
19 url: string
20 registrationToken: string
21 runnerName: string
22 runnerDescription?: string
23 }) {
24 const req: IPCRequest = {
25 type: 'register',
26 ...options
27 }
28
29 const { success, error } = await this.netIPC.request(req) as IPCReponse
30
31 if (success) console.log('PeerTube instance registered')
32 else console.error('Could not register PeerTube instance on runner server side', error)
33 }
34
35 async askUnregister (options: {
36 url: string
37 }) {
38 const req: IPCRequest = {
39 type: 'unregister',
40 ...options
41 }
42
43 const { success, error } = await this.netIPC.request(req) as IPCReponse
44
45 if (success) console.log('PeerTube instance unregistered')
46 else console.error('Could not unregister PeerTube instance on runner server side', error)
47 }
48
49 async askListRegistered () {
50 const req: IPCRequest = {
51 type: 'list-registered'
52 }
53
54 const { success, error, data } = await this.netIPC.request(req) as IPCReponse<IPCReponseData>
55 if (!success) {
56 console.error('Could not list registered PeerTube instances', error)
57 return
58 }
59
60 const table = new CliTable3({
61 head: [ 'instance', 'runner name', 'runner description' ]
62 })
63
64 for (const server of data.servers) {
65 table.push([ server.url, server.runnerName, server.runnerDescription ])
66 }
67
68 console.log(table.toString())
69 }
70
71 stop () {
72 this.netIPC.destroy()
73 }
74}
diff --git a/packages/peertube-runner/shared/ipc/ipc-server.ts b/packages/peertube-runner/shared/ipc/ipc-server.ts
new file mode 100644
index 000000000..bc340198b
--- /dev/null
+++ b/packages/peertube-runner/shared/ipc/ipc-server.ts
@@ -0,0 +1,61 @@
1import { ensureDir } from 'fs-extra'
2import { Server as NetIPC } from 'net-ipc'
3import { pick } from '@shared/core-utils'
4import { RunnerServer } from '../../server'
5import { ConfigManager } from '../config-manager'
6import { logger } from '../logger'
7import { IPCReponse, IPCReponseData, IPCRequest } from './shared'
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 console.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({ url: req.url })
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 => console.error('Cannot send response after IPC request', err))
60 }
61}
diff --git a/packages/peertube-runner/shared/ipc/shared/index.ts b/packages/peertube-runner/shared/ipc/shared/index.ts
new file mode 100644
index 000000000..deaaa152e
--- /dev/null
+++ b/packages/peertube-runner/shared/ipc/shared/index.ts
@@ -0,0 +1,2 @@
1export * from './ipc-request.model'
2export * from './ipc-response.model'
diff --git a/packages/peertube-runner/shared/ipc/shared/ipc-request.model.ts b/packages/peertube-runner/shared/ipc/shared/ipc-request.model.ts
new file mode 100644
index 000000000..0f733cdfe
--- /dev/null
+++ b/packages/peertube-runner/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 }
15export type IPCRequestListRegistered = { type: 'list-registered' }
diff --git a/packages/peertube-runner/shared/ipc/shared/ipc-response.model.ts b/packages/peertube-runner/shared/ipc/shared/ipc-response.model.ts
new file mode 100644
index 000000000..689d6e09a
--- /dev/null
+++ b/packages/peertube-runner/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/packages/peertube-runner/shared/logger.ts b/packages/peertube-runner/shared/logger.ts
new file mode 100644
index 000000000..bf0f41828
--- /dev/null
+++ b/packages/peertube-runner/shared/logger.ts
@@ -0,0 +1,12 @@
1import { pino } from 'pino'
2import pretty from 'pino-pretty'
3
4const logger = pino(pretty({
5 colorize: true
6}))
7
8logger.level = 'info'
9
10export {
11 logger
12}
diff --git a/packages/peertube-runner/tsconfig.json b/packages/peertube-runner/tsconfig.json
new file mode 100644
index 000000000..b6c62bc34
--- /dev/null
+++ b/packages/peertube-runner/tsconfig.json
@@ -0,0 +1,9 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "outDir": "./dist"
5 },
6 "references": [
7 { "path": "../../shared" }
8 ]
9}
diff --git a/packages/peertube-runner/yarn.lock b/packages/peertube-runner/yarn.lock
new file mode 100644
index 000000000..adb5aa118
--- /dev/null
+++ b/packages/peertube-runner/yarn.lock
@@ -0,0 +1,528 @@
1# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2# yarn lockfile v1
3
4
5"@commander-js/extra-typings@^10.0.3":
6 version "10.0.3"
7 resolved "https://registry.yarnpkg.com/@commander-js/extra-typings/-/extra-typings-10.0.3.tgz#8b6c64897231ed9c00461db82018b5131b653aae"
8 integrity sha512-OIw28QV/GlP8k0B5CJTRsl8IyNvd0R8C8rfo54Yz9P388vCNDgdNrFlKxZTGqps+5j6lSw3Ss9JTQwcur1w1oA==
9
10"@esbuild/android-arm64@0.17.15":
11 version "0.17.15"
12 resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.15.tgz#893ad71f3920ccb919e1757c387756a9bca2ef42"
13 integrity sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==
14
15"@esbuild/android-arm@0.17.15":
16 version "0.17.15"
17 resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.15.tgz#143e0d4e4c08c786ea410b9a7739779a9a1315d8"
18 integrity sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==
19
20"@esbuild/android-x64@0.17.15":
21 version "0.17.15"
22 resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.15.tgz#d2d12a7676b2589864281b2274355200916540bc"
23 integrity sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==
24
25"@esbuild/darwin-arm64@0.17.15":
26 version "0.17.15"
27 resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.15.tgz#2e88e79f1d327a2a7d9d06397e5232eb0a473d61"
28 integrity sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==
29
30"@esbuild/darwin-x64@0.17.15":
31 version "0.17.15"
32 resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.15.tgz#9384e64c0be91388c57be6d3a5eaf1c32a99c91d"
33 integrity sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==
34
35"@esbuild/freebsd-arm64@0.17.15":
36 version "0.17.15"
37 resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.15.tgz#2ad5a35bc52ebd9ca6b845dbc59ba39647a93c1a"
38 integrity sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==
39
40"@esbuild/freebsd-x64@0.17.15":
41 version "0.17.15"
42 resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.15.tgz#b513a48446f96c75fda5bef470e64d342d4379cd"
43 integrity sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==
44
45"@esbuild/linux-arm64@0.17.15":
46 version "0.17.15"
47 resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.15.tgz#9697b168175bfd41fa9cc4a72dd0d48f24715f31"
48 integrity sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==
49
50"@esbuild/linux-arm@0.17.15":
51 version "0.17.15"
52 resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.15.tgz#5b22062c54f48cd92fab9ffd993732a52db70cd3"
53 integrity sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==
54
55"@esbuild/linux-ia32@0.17.15":
56 version "0.17.15"
57 resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.15.tgz#eb28a13f9b60b5189fcc9e98e1024f6b657ba54c"
58 integrity sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==
59
60"@esbuild/linux-loong64@0.17.15":
61 version "0.17.15"
62 resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.15.tgz#32454bdfe144cf74b77895a8ad21a15cb81cfbe5"
63 integrity sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==
64
65"@esbuild/linux-mips64el@0.17.15":
66 version "0.17.15"
67 resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.15.tgz#af12bde0d775a318fad90eb13a0455229a63987c"
68 integrity sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==
69
70"@esbuild/linux-ppc64@0.17.15":
71 version "0.17.15"
72 resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.15.tgz#34c5ed145b2dfc493d3e652abac8bd3baa3865a5"
73 integrity sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==
74
75"@esbuild/linux-riscv64@0.17.15":
76 version "0.17.15"
77 resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.15.tgz#87bd515e837f2eb004b45f9e6a94dc5b93f22b92"
78 integrity sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==
79
80"@esbuild/linux-s390x@0.17.15":
81 version "0.17.15"
82 resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.15.tgz#20bf7947197f199ddac2ec412029a414ceae3aa3"
83 integrity sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==
84
85"@esbuild/linux-x64@0.17.15":
86 version "0.17.15"
87 resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.15.tgz#31b93f9c94c195e852c20cd3d1914a68aa619124"
88 integrity sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==
89
90"@esbuild/netbsd-x64@0.17.15":
91 version "0.17.15"
92 resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.15.tgz#8da299b3ac6875836ca8cdc1925826498069ac65"
93 integrity sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==
94
95"@esbuild/openbsd-x64@0.17.15":
96 version "0.17.15"
97 resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.15.tgz#04a1ec3d4e919714dba68dcf09eeb1228ad0d20c"
98 integrity sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==
99
100"@esbuild/sunos-x64@0.17.15":
101 version "0.17.15"
102 resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.15.tgz#6694ebe4e16e5cd7dab6505ff7c28f9c1c695ce5"
103 integrity sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==
104
105"@esbuild/win32-arm64@0.17.15":
106 version "0.17.15"
107 resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.15.tgz#1f95b2564193c8d1fee8f8129a0609728171d500"
108 integrity sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==
109
110"@esbuild/win32-ia32@0.17.15":
111 version "0.17.15"
112 resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.15.tgz#c362b88b3df21916ed7bcf75c6d09c6bf3ae354a"
113 integrity sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==
114
115"@esbuild/win32-x64@0.17.15":
116 version "0.17.15"
117 resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz#c2e737f3a201ebff8e2ac2b8e9f246b397ad19b8"
118 integrity sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==
119
120"@iarna/toml@^2.2.5":
121 version "2.2.5"
122 resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
123 integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==
124
125"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2":
126 version "3.0.2"
127 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38"
128 integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==
129
130"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2":
131 version "3.0.2"
132 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz#f954f34355712212a8e06c465bc06c40852c6bb3"
133 integrity sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==
134
135"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2":
136 version "3.0.2"
137 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz#45c63037f045c2b15c44f80f0393fa24f9655367"
138 integrity sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==
139
140"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2":
141 version "3.0.2"
142 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz#35707efeafe6d22b3f373caf9e8775e8920d1399"
143 integrity sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==
144
145"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2":
146 version "3.0.2"
147 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz#091b1218b66c341f532611477ef89e83f25fae4f"
148 integrity sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==
149
150"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2":
151 version "3.0.2"
152 resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407"
153 integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==
154
155abort-controller@^3.0.0:
156 version "3.0.0"
157 resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
158 integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
159 dependencies:
160 event-target-shim "^5.0.0"
161
162atomic-sleep@^1.0.0:
163 version "1.0.0"
164 resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
165 integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
166
167balanced-match@^1.0.0:
168 version "1.0.2"
169 resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
170 integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
171
172base64-js@^1.3.1:
173 version "1.5.1"
174 resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
175 integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
176
177brace-expansion@^2.0.1:
178 version "2.0.1"
179 resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
180 integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
181 dependencies:
182 balanced-match "^1.0.0"
183
184buffer@^6.0.3:
185 version "6.0.3"
186 resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
187 integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
188 dependencies:
189 base64-js "^1.3.1"
190 ieee754 "^1.2.1"
191
192colorette@^2.0.7:
193 version "2.0.19"
194 resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
195 integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
196
197dateformat@^4.6.3:
198 version "4.6.3"
199 resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
200 integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
201
202end-of-stream@^1.1.0:
203 version "1.4.4"
204 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
205 integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
206 dependencies:
207 once "^1.4.0"
208
209env-paths@^3.0.0:
210 version "3.0.0"
211 resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da"
212 integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==
213
214esbuild@^0.17.15:
215 version "0.17.15"
216 resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.15.tgz#209ebc87cb671ffb79574db93494b10ffaf43cbc"
217 integrity sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==
218 optionalDependencies:
219 "@esbuild/android-arm" "0.17.15"
220 "@esbuild/android-arm64" "0.17.15"
221 "@esbuild/android-x64" "0.17.15"
222 "@esbuild/darwin-arm64" "0.17.15"
223 "@esbuild/darwin-x64" "0.17.15"
224 "@esbuild/freebsd-arm64" "0.17.15"
225 "@esbuild/freebsd-x64" "0.17.15"
226 "@esbuild/linux-arm" "0.17.15"
227 "@esbuild/linux-arm64" "0.17.15"
228 "@esbuild/linux-ia32" "0.17.15"
229 "@esbuild/linux-loong64" "0.17.15"
230 "@esbuild/linux-mips64el" "0.17.15"
231 "@esbuild/linux-ppc64" "0.17.15"
232 "@esbuild/linux-riscv64" "0.17.15"
233 "@esbuild/linux-s390x" "0.17.15"
234 "@esbuild/linux-x64" "0.17.15"
235 "@esbuild/netbsd-x64" "0.17.15"
236 "@esbuild/openbsd-x64" "0.17.15"
237 "@esbuild/sunos-x64" "0.17.15"
238 "@esbuild/win32-arm64" "0.17.15"
239 "@esbuild/win32-ia32" "0.17.15"
240 "@esbuild/win32-x64" "0.17.15"
241
242event-target-shim@^5.0.0:
243 version "5.0.1"
244 resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
245 integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
246
247events@^3.3.0:
248 version "3.3.0"
249 resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
250 integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
251
252fast-copy@^3.0.0:
253 version "3.0.1"
254 resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa"
255 integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==
256
257fast-redact@^3.1.1:
258 version "3.1.2"
259 resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
260 integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
261
262fast-safe-stringify@^2.1.1:
263 version "2.1.1"
264 resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
265 integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
266
267fast-zlib@^2.0.1:
268 version "2.0.1"
269 resolved "https://registry.yarnpkg.com/fast-zlib/-/fast-zlib-2.0.1.tgz#be624f592fc80ad8019ee2025d16a367a4e9b024"
270 integrity sha512-DCoYgNagM2Bt1VIpXpdGnRx4LzqJeYG0oh6Nf/7cWo6elTXkFGMw9CrRCYYUIapYNrozYMoyDRflx9mgT3Awyw==
271
272fs.realpath@^1.0.0:
273 version "1.0.0"
274 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
275 integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
276
277glob@^8.0.0:
278 version "8.1.0"
279 resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
280 integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
281 dependencies:
282 fs.realpath "^1.0.0"
283 inflight "^1.0.4"
284 inherits "2"
285 minimatch "^5.0.1"
286 once "^1.3.0"
287
288help-me@^4.0.1:
289 version "4.2.0"
290 resolved "https://registry.yarnpkg.com/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563"
291 integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==
292 dependencies:
293 glob "^8.0.0"
294 readable-stream "^3.6.0"
295
296ieee754@^1.2.1:
297 version "1.2.1"
298 resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
299 integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
300
301inflight@^1.0.4:
302 version "1.0.6"
303 resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
304 integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
305 dependencies:
306 once "^1.3.0"
307 wrappy "1"
308
309inherits@2, inherits@^2.0.3:
310 version "2.0.4"
311 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
312 integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
313
314joycon@^3.1.1:
315 version "3.1.1"
316 resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
317 integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
318
319minimatch@^5.0.1:
320 version "5.1.6"
321 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
322 integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
323 dependencies:
324 brace-expansion "^2.0.1"
325
326minimist@^1.2.6:
327 version "1.2.8"
328 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
329 integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
330
331msgpackr-extract@^3.0.1:
332 version "3.0.2"
333 resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d"
334 integrity sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==
335 dependencies:
336 node-gyp-build-optional-packages "5.0.7"
337 optionalDependencies:
338 "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.2"
339 "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.2"
340 "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.2"
341 "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.2"
342 "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.2"
343 "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.2"
344
345msgpackr@^1.3.2:
346 version "1.8.5"
347 resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.5.tgz#8cadfb935357680648f33699d0e833c9179dbfeb"
348 integrity sha512-mpPs3qqTug6ahbblkThoUY2DQdNXcm4IapwOS3Vm/87vmpzLVelvp9h3It1y9l1VPpiFLV11vfOXnmeEwiIXwg==
349 optionalDependencies:
350 msgpackr-extract "^3.0.1"
351
352net-ipc@^2.0.1:
353 version "2.0.1"
354 resolved "https://registry.yarnpkg.com/net-ipc/-/net-ipc-2.0.1.tgz#1da79ca16f1624f2ed1099a124cb065912c595a5"
355 integrity sha512-4HLjZ/Xorj4kxA7WUajF2EAXlS+OR+XliDLkqQA53Wm7eIr/hWLjdXt4zzB6q4Ii8BB+HbuRbM9yLov3+ttRUw==
356 optionalDependencies:
357 fast-zlib "^2.0.1"
358 msgpackr "^1.3.2"
359
360node-gyp-build-optional-packages@5.0.7:
361 version "5.0.7"
362 resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3"
363 integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==
364
365on-exit-leak-free@^2.1.0:
366 version "2.1.0"
367 resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
368 integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==
369
370once@^1.3.0, once@^1.3.1, once@^1.4.0:
371 version "1.4.0"
372 resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
373 integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
374 dependencies:
375 wrappy "1"
376
377pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
378 version "1.0.0"
379 resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
380 integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
381 dependencies:
382 readable-stream "^4.0.0"
383 split2 "^4.0.0"
384
385pino-pretty@^10.0.0:
386 version "10.0.0"
387 resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-10.0.0.tgz#fd2f307ee897289f63d09b0b804ac2ecc9a18516"
388 integrity sha512-zKFjYXBzLaLTEAN1ayKpHXtL5UeRQC7R3lvhKe7fWs7hIVEjKGG/qIXwQt9HmeUp71ogUd/YcW+LmMwRp4KT6Q==
389 dependencies:
390 colorette "^2.0.7"
391 dateformat "^4.6.3"
392 fast-copy "^3.0.0"
393 fast-safe-stringify "^2.1.1"
394 help-me "^4.0.1"
395 joycon "^3.1.1"
396 minimist "^1.2.6"
397 on-exit-leak-free "^2.1.0"
398 pino-abstract-transport "^1.0.0"
399 pump "^3.0.0"
400 readable-stream "^4.0.0"
401 secure-json-parse "^2.4.0"
402 sonic-boom "^3.0.0"
403 strip-json-comments "^3.1.1"
404
405pino-std-serializers@^6.0.0:
406 version "6.2.0"
407 resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz#169048c0df3f61352fce56aeb7fb962f1b66ab43"
408 integrity sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==
409
410pino@^8.11.0:
411 version "8.11.0"
412 resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498"
413 integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==
414 dependencies:
415 atomic-sleep "^1.0.0"
416 fast-redact "^3.1.1"
417 on-exit-leak-free "^2.1.0"
418 pino-abstract-transport v1.0.0
419 pino-std-serializers "^6.0.0"
420 process-warning "^2.0.0"
421 quick-format-unescaped "^4.0.3"
422 real-require "^0.2.0"
423 safe-stable-stringify "^2.3.1"
424 sonic-boom "^3.1.0"
425 thread-stream "^2.0.0"
426
427process-warning@^2.0.0:
428 version "2.2.0"
429 resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.2.0.tgz#008ec76b579820a8e5c35d81960525ca64feb626"
430 integrity sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==
431
432process@^0.11.10:
433 version "0.11.10"
434 resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
435 integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
436
437pump@^3.0.0:
438 version "3.0.0"
439 resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
440 integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
441 dependencies:
442 end-of-stream "^1.1.0"
443 once "^1.3.1"
444
445quick-format-unescaped@^4.0.3:
446 version "4.0.4"
447 resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
448 integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
449
450readable-stream@^3.6.0:
451 version "3.6.2"
452 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
453 integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
454 dependencies:
455 inherits "^2.0.3"
456 string_decoder "^1.1.1"
457 util-deprecate "^1.0.1"
458
459readable-stream@^4.0.0:
460 version "4.3.0"
461 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba"
462 integrity sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==
463 dependencies:
464 abort-controller "^3.0.0"
465 buffer "^6.0.3"
466 events "^3.3.0"
467 process "^0.11.10"
468
469real-require@^0.2.0:
470 version "0.2.0"
471 resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
472 integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
473
474safe-buffer@~5.2.0:
475 version "5.2.1"
476 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
477 integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
478
479safe-stable-stringify@^2.3.1:
480 version "2.4.3"
481 resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886"
482 integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
483
484secure-json-parse@^2.4.0:
485 version "2.7.0"
486 resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
487 integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
488
489sonic-boom@^3.0.0, sonic-boom@^3.1.0:
490 version "3.3.0"
491 resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c"
492 integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==
493 dependencies:
494 atomic-sleep "^1.0.0"
495
496split2@^4.0.0:
497 version "4.2.0"
498 resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
499 integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
500
501string_decoder@^1.1.1:
502 version "1.3.0"
503 resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
504 integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
505 dependencies:
506 safe-buffer "~5.2.0"
507
508strip-json-comments@^3.1.1:
509 version "3.1.1"
510 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
511 integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
512
513thread-stream@^2.0.0:
514 version "2.3.0"
515 resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.3.0.tgz#4fc07fb39eff32ae7bad803cb7dd9598349fed33"
516 integrity sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==
517 dependencies:
518 real-require "^0.2.0"
519
520util-deprecate@^1.0.1:
521 version "1.0.2"
522 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
523 integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
524
525wrappy@1:
526 version "1.0.2"
527 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
528 integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==