diff options
author | Chocobozzz <me@florianbigard.com> | 2023-04-21 15:05:27 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2023-05-09 08:57:34 +0200 |
commit | 1772b383de490cf406fe93ef3aa3a941f6db513c (patch) | |
tree | 7cecc404c8d71951c22079e9bf5180095981b7f9 /packages/peertube-runner | |
parent | 118626c8752bee7b05c4e0b668852e1aba2416f1 (diff) | |
download | PeerTube-1772b383de490cf406fe93ef3aa3a941f6db513c.tar.gz PeerTube-1772b383de490cf406fe93ef3aa3a941f6db513c.tar.zst PeerTube-1772b383de490cf406fe93ef3aa3a941f6db513c.zip |
Add peertube runner cli
Diffstat (limited to 'packages/peertube-runner')
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 @@ | |||
1 | node_modules | ||
2 | dist | ||
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 @@ | |||
1 | import { Command, InvalidArgumentError } from '@commander-js/extra-typings' | ||
2 | import { listRegistered, registerRunner, unregisterRunner } from './register' | ||
3 | import { RunnerServer } from './server' | ||
4 | import { ConfigManager, logger } from './shared' | ||
5 | |||
6 | const 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 | |||
23 | program.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 | |||
34 | program.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 | |||
49 | program.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 | |||
61 | program.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 | |||
72 | program.parse() | ||
73 | |||
74 | // --------------------------------------------------------------------------- | ||
75 | // Private | ||
76 | // --------------------------------------------------------------------------- | ||
77 | |||
78 | function 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 @@ | |||
1 | import { IPCClient } from '../shared/ipc' | ||
2 | |||
3 | export 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 | |||
17 | export 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 | |||
28 | export 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 @@ | |||
1 | export * from './shared' | ||
2 | export * 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 @@ | |||
1 | import { logger } from 'packages/peertube-runner/shared/logger' | ||
2 | import { | ||
3 | RunnerJobLiveRTMPHLSTranscodingPayload, | ||
4 | RunnerJobVODAudioMergeTranscodingPayload, | ||
5 | RunnerJobVODHLSTranscodingPayload, | ||
6 | RunnerJobVODWebVideoTranscodingPayload | ||
7 | } from '@shared/models' | ||
8 | import { processAudioMergeTranscoding, processHLSTranscoding, ProcessOptions, processWebVideoTranscoding } from './shared' | ||
9 | import { ProcessLiveRTMPHLSTranscoding } from './shared/process-live' | ||
10 | |||
11 | export 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 @@ | |||
1 | import { throttle } from 'lodash' | ||
2 | import { ConfigManager, downloadFile, logger } from 'packages/peertube-runner/shared' | ||
3 | import { join } from 'path' | ||
4 | import { buildUUID } from '@shared/extra-utils' | ||
5 | import { FFmpegLive, FFmpegVOD } from '@shared/ffmpeg' | ||
6 | import { RunnerJob, RunnerJobPayload } from '@shared/models' | ||
7 | import { PeerTubeServer } from '@shared/server-commands' | ||
8 | import { getTranscodingLogger } from './transcoding-logger' | ||
9 | import { getAvailableEncoders, getEncodersToTry } from './transcoding-profiles' | ||
10 | |||
11 | export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string } | ||
12 | |||
13 | export type ProcessOptions <T extends RunnerJobPayload = RunnerJobPayload> = { | ||
14 | server: PeerTubeServer | ||
15 | job: JobWithToken<T> | ||
16 | runnerToken: string | ||
17 | } | ||
18 | |||
19 | export 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 | |||
32 | export 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 | |||
43 | export 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 | |||
77 | export 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 @@ | |||
1 | export * from './common' | ||
2 | export * from './process-vod' | ||
3 | export * from './transcoding-logger' | ||
4 | export * 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 @@ | |||
1 | import { FSWatcher, watch } from 'chokidar' | ||
2 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
3 | import { ensureDir, remove } from 'fs-extra' | ||
4 | import { logger } from 'packages/peertube-runner/shared' | ||
5 | import { basename, join } from 'path' | ||
6 | import { wait } from '@shared/core-utils' | ||
7 | import { buildUUID } from '@shared/extra-utils' | ||
8 | import { ffprobePromise, getVideoStreamBitrate, getVideoStreamDimensionsInfo, hasAudioStream } from '@shared/ffmpeg' | ||
9 | import { | ||
10 | LiveRTMPHLSTranscodingSuccess, | ||
11 | LiveRTMPHLSTranscodingUpdatePayload, | ||
12 | PeerTubeProblemDocument, | ||
13 | RunnerJobLiveRTMPHLSTranscodingPayload, | ||
14 | ServerErrorCode | ||
15 | } from '@shared/models' | ||
16 | import { ConfigManager } from '../../../shared/config-manager' | ||
17 | import { buildFFmpegLive, ProcessOptions } from './common' | ||
18 | |||
19 | export 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 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { buildUUID } from '@shared/extra-utils' | ||
4 | import { | ||
5 | RunnerJobVODAudioMergeTranscodingPayload, | ||
6 | RunnerJobVODHLSTranscodingPayload, | ||
7 | RunnerJobVODWebVideoTranscodingPayload, | ||
8 | VODAudioMergeTranscodingSuccess, | ||
9 | VODHLSTranscodingSuccess, | ||
10 | VODWebVideoTranscodingSuccess | ||
11 | } from '@shared/models' | ||
12 | import { ConfigManager } from '../../../shared/config-manager' | ||
13 | import { buildFFmpegVOD, downloadInputFile, ProcessOptions } from './common' | ||
14 | |||
15 | export 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 | |||
52 | export 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 | |||
94 | export 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 @@ | |||
1 | import { logger } from 'packages/peertube-runner/shared/logger' | ||
2 | |||
3 | export 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 @@ | |||
1 | import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils' | ||
2 | import { buildStreamSuffix, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '@shared/ffmpeg' | ||
3 | import { EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '@shared/models' | ||
4 | |||
5 | const 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 | |||
19 | const 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 | |||
34 | const 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 | |||
56 | const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => { | ||
57 | return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } | ||
58 | } | ||
59 | |||
60 | export 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 | |||
84 | export 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 | |||
100 | function 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 | |||
114 | function 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 | |||
123 | function 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 @@ | |||
1 | import { ensureDir, readdir, remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { io, Socket } from 'socket.io-client' | ||
4 | import { pick } from '@shared/core-utils' | ||
5 | import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models' | ||
6 | import { PeerTubeServer as PeerTubeServerCommand } from '@shared/server-commands' | ||
7 | import { ConfigManager } from '../shared' | ||
8 | import { IPCServer } from '../shared/ipc' | ||
9 | import { logger } from '../shared/logger' | ||
10 | import { JobWithToken, processJob } from './process' | ||
11 | |||
12 | type PeerTubeServer = PeerTubeServerCommand & { | ||
13 | runnerToken: string | ||
14 | runnerName: string | ||
15 | runnerDescription?: string | ||
16 | } | ||
17 | |||
18 | export 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 @@ | |||
1 | import envPaths from 'env-paths' | ||
2 | import { ensureDir, pathExists, readFile, remove, writeFile } from 'fs-extra' | ||
3 | import { merge } from 'lodash' | ||
4 | import { logger } from 'packages/peertube-runner/shared/logger' | ||
5 | import { dirname, join } from 'path' | ||
6 | import { parse, stringify } from '@iarna/toml' | ||
7 | |||
8 | const paths = envPaths('peertube-runner') | ||
9 | |||
10 | type 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 | |||
28 | export 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 @@ | |||
1 | import { createWriteStream, remove } from 'fs-extra' | ||
2 | import { request as requestHTTP } from 'http' | ||
3 | import { request as requestHTTPS, RequestOptions } from 'https' | ||
4 | import { logger } from './logger' | ||
5 | |||
6 | export 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 | |||
62 | function 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 @@ | |||
1 | export * from './config-manager' | ||
2 | export * from './http' | ||
3 | export * 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 @@ | |||
1 | export * from './ipc-client' | ||
2 | export * 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 @@ | |||
1 | import CliTable3 from 'cli-table3' | ||
2 | import { ensureDir } from 'fs-extra' | ||
3 | import { Client as NetIPC } from 'net-ipc' | ||
4 | import { ConfigManager } from '../config-manager' | ||
5 | import { IPCReponse, IPCReponseData, IPCRequest } from './shared' | ||
6 | |||
7 | export 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 @@ | |||
1 | import { ensureDir } from 'fs-extra' | ||
2 | import { Server as NetIPC } from 'net-ipc' | ||
3 | import { pick } from '@shared/core-utils' | ||
4 | import { RunnerServer } from '../../server' | ||
5 | import { ConfigManager } from '../config-manager' | ||
6 | import { logger } from '../logger' | ||
7 | import { IPCReponse, IPCReponseData, IPCRequest } from './shared' | ||
8 | |||
9 | export 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 @@ | |||
1 | export * from './ipc-request.model' | ||
2 | export * 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 @@ | |||
1 | export type IPCRequest = | ||
2 | IPCRequestRegister | | ||
3 | IPCRequestUnregister | | ||
4 | IPCRequestListRegistered | ||
5 | |||
6 | export type IPCRequestRegister = { | ||
7 | type: 'register' | ||
8 | url: string | ||
9 | registrationToken: string | ||
10 | runnerName: string | ||
11 | runnerDescription?: string | ||
12 | } | ||
13 | |||
14 | export type IPCRequestUnregister = { type: 'unregister', url: string } | ||
15 | export 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 @@ | |||
1 | export type IPCReponse <T extends IPCReponseData = undefined> = { | ||
2 | success: boolean | ||
3 | error?: string | ||
4 | data?: T | ||
5 | } | ||
6 | |||
7 | export 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 @@ | |||
1 | import { pino } from 'pino' | ||
2 | import pretty from 'pino-pretty' | ||
3 | |||
4 | const logger = pino(pretty({ | ||
5 | colorize: true | ||
6 | })) | ||
7 | |||
8 | logger.level = 'info' | ||
9 | |||
10 | export { | ||
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 | |||
155 | abort-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 | |||
162 | atomic-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 | |||
167 | balanced-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 | |||
172 | base64-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 | |||
177 | brace-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 | |||
184 | buffer@^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 | |||
192 | colorette@^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 | |||
197 | dateformat@^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 | |||
202 | end-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 | |||
209 | env-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 | |||
214 | esbuild@^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 | |||
242 | event-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 | |||
247 | events@^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 | |||
252 | fast-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 | |||
257 | fast-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 | |||
262 | fast-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 | |||
267 | fast-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 | |||
272 | fs.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 | |||
277 | glob@^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 | |||
288 | help-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 | |||
296 | ieee754@^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 | |||
301 | inflight@^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 | |||
309 | inherits@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 | |||
314 | joycon@^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 | |||
319 | minimatch@^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 | |||
326 | minimist@^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 | |||
331 | msgpackr-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 | |||
345 | msgpackr@^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 | |||
352 | net-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 | |||
360 | node-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 | |||
365 | on-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 | |||
370 | once@^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 | |||
377 | pino-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 | |||
385 | pino-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 | |||
405 | pino-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 | |||
410 | pino@^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 | |||
427 | process-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 | |||
432 | process@^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 | |||
437 | pump@^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 | |||
445 | quick-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 | |||
450 | readable-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 | |||
459 | readable-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 | |||
469 | real-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 | |||
474 | safe-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 | |||
479 | safe-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 | |||
484 | secure-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 | |||
489 | sonic-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 | |||
496 | split2@^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 | |||
501 | string_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 | |||
508 | strip-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 | |||
513 | thread-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 | |||
520 | util-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 | |||
525 | wrappy@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== | ||