]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - shared/extra-utils/server/servers.ts
Introduce config command
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / server / servers.ts
CommitLineData
a1587156 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
89231874 2
c655c9ef 3import { expect } from 'chai'
0e1dc3e7 4import { ChildProcess, exec, fork } from 'child_process'
83ef31fe 5import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
c655c9ef 6import { join } from 'path'
7c3b7976 7import { randomInt } from '../../core-utils/miscs/miscs'
a35a2279 8import { VideoChannel } from '../../models/videos'
329619b3
C
9import { BulkCommand } from '../bulk'
10import { CLICommand } from '../cli'
e8bd7ce7 11import { CustomPagesCommand } from '../custom-pages'
c1bc8ee4 12import { FeedCommand } from '../feeds'
a92ddacb 13import { LogsCommand } from '../logs'
83ef31fe 14import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
0c1a77e9 15import { AbusesCommand } from '../moderation'
23a3a882 16import { OverviewsCommand } from '../overviews'
78d62f4d 17import { makeGetRequest } from '../requests/requests'
af971e06 18import { SearchCommand } from '../search'
65e6e260 19import { ConfigCommand } from './config-command'
a9c58393 20import { ContactFormCommand } from './contact-form-command'
883a9019 21import { DebugCommand } from './debug-command'
c3d29f69 22import { FollowsCommand } from './follows-command'
9c6327f8 23import { JobsCommand } from './jobs-command'
ae2abfd3 24import { PluginsCommand } from './plugins-command'
dab04709 25import { RedundancyCommand } from './redundancy-command'
bc809041 26import { StatsCommand } from './stats-command'
0e1dc3e7
C
27
28interface ServerInfo {
a1587156 29 app: ChildProcess
af4ae64f 30
0e1dc3e7
C
31 url: string
32 host: string
af4ae64f 33 hostname: string
86ebdf8c 34 port: number
af4ae64f 35
c655c9ef
C
36 rtmpPort: number
37
86ebdf8c
C
38 parallel: boolean
39 internalServerNumber: number
fdbda9e3 40 serverNumber: number
0e1dc3e7
C
41
42 client: {
a1587156 43 id: string
0e1dc3e7
C
44 secret: string
45 }
46
47 user: {
a1587156
C
48 username: string
49 password: string
0e1dc3e7
C
50 email?: string
51 }
52
7c3b7976
C
53 customConfigFile?: string
54
0e1dc3e7 55 accessToken?: string
f43db2f4 56 refreshToken?: string
df0b219d 57 videoChannel?: VideoChannel
0e1dc3e7
C
58
59 video?: {
60 id: number
61 uuid: string
d4a8e7a6 62 shortUUID: string
310b5219 63 name?: string
a59f210f 64 url?: string
aea0b0e7 65
310b5219 66 account?: {
b64c950a
C
67 name: string
68 }
aea0b0e7
C
69
70 embedPath?: string
0e1dc3e7
C
71 }
72
73 remoteVideo?: {
74 id: number
75 uuid: string
76 }
df0b219d
C
77
78 videos?: { id: number, uuid: string }[]
329619b3
C
79
80 bulkCommand?: BulkCommand
81 cliCommand?: CLICommand
e8bd7ce7 82 customPageCommand?: CustomPagesCommand
c1bc8ee4 83 feedCommand?: FeedCommand
a92ddacb 84 logsCommand?: LogsCommand
0c1a77e9 85 abusesCommand?: AbusesCommand
23a3a882 86 overviewsCommand?: OverviewsCommand
af971e06 87 searchCommand?: SearchCommand
a9c58393 88 contactFormCommand?: ContactFormCommand
883a9019 89 debugCommand?: DebugCommand
c3d29f69 90 followsCommand?: FollowsCommand
9c6327f8 91 jobsCommand?: JobsCommand
ae2abfd3 92 pluginsCommand?: PluginsCommand
dab04709 93 redundancyCommand?: RedundancyCommand
bc809041 94 statsCommand?: StatsCommand
65e6e260 95 configCommand?: ConfigCommand
0e1dc3e7
C
96}
97
7c3b7976
C
98function parallelTests () {
99 return process.env.MOCHA_PARALLEL === 'true'
100}
101
b36f41ca 102function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
a1587156 103 const apps = []
0e1dc3e7
C
104 let i = 0
105
106 return new Promise<ServerInfo[]>(res => {
107 function anotherServerDone (serverNumber, app) {
108 apps[serverNumber - 1] = app
109 i++
110 if (i === totalServers) {
111 return res(apps)
112 }
113 }
114
210feb6c
C
115 for (let j = 1; j <= totalServers; j++) {
116 flushAndRunServer(j, configOverride).then(app => anotherServerDone(j, app))
117 }
0e1dc3e7
C
118 })
119}
120
210feb6c 121function flushTests (serverNumber?: number) {
0e1dc3e7 122 return new Promise<void>((res, rej) => {
210feb6c
C
123 const suffix = serverNumber ? ` -- ${serverNumber}` : ''
124
2284f202
C
125 return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
126 if (err || stderr) return rej(err || new Error(stderr))
0e1dc3e7
C
127
128 return res()
129 })
130 })
131}
132
86ebdf8c
C
133function randomServer () {
134 const low = 10
135 const high = 10000
136
7c3b7976 137 return randomInt(low, high)
86ebdf8c
C
138}
139
c655c9ef
C
140function randomRTMP () {
141 const low = 1900
142 const high = 2100
143
144 return randomInt(low, high)
145}
146
bd2e2f11
C
147type RunServerOptions = {
148 hideLogs?: boolean
149 execArgv?: string[]
150}
151
152async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = [], options: RunServerOptions = {}) {
7c3b7976 153 const parallel = parallelTests()
86ebdf8c
C
154
155 const internalServerNumber = parallel ? randomServer() : serverNumber
3e8584b9 156 const rtmpPort = parallel ? randomRTMP() : 1936
86ebdf8c
C
157 const port = 9000 + internalServerNumber
158
7c3b7976 159 await flushTests(internalServerNumber)
42e1ec25 160
0e1dc3e7
C
161 const server: ServerInfo = {
162 app: null,
86ebdf8c
C
163 port,
164 internalServerNumber,
c655c9ef 165 rtmpPort,
86ebdf8c 166 parallel,
7c3b7976 167 serverNumber,
86ebdf8c
C
168 url: `http://localhost:${port}`,
169 host: `localhost:${port}`,
af4ae64f 170 hostname: 'localhost',
0e1dc3e7
C
171 client: {
172 id: null,
173 secret: null
174 },
175 user: {
176 username: null,
177 password: null
178 }
179 }
180
bd2e2f11 181 return runServer(server, configOverride, args, options)
913b1d71
C
182}
183
bd2e2f11 184async function runServer (server: ServerInfo, configOverrideArg?: any, args = [], options: RunServerOptions = {}) {
0e1dc3e7
C
185 // These actions are async so we need to be sure that they have both been done
186 const serverRunString = {
fcb77122 187 'HTTP server listening': false
0e1dc3e7 188 }
913b1d71 189 const key = 'Database peertube_test' + server.internalServerNumber + ' is ready'
0e1dc3e7
C
190 serverRunString[key] = false
191
192 const regexps = {
193 client_id: 'Client id: (.+)',
194 client_secret: 'Client secret: (.+)',
195 user_username: 'Username: (.+)',
196 user_password: 'User password: (.+)'
197 }
198
7c3b7976
C
199 if (server.internalServerNumber !== server.serverNumber) {
200 const basePath = join(root(), 'config')
201
202 const tmpConfigFile = join(basePath, `test-${server.internalServerNumber}.yaml`)
203 await copy(join(basePath, `test-${server.serverNumber}.yaml`), tmpConfigFile)
204
205 server.customConfigFile = tmpConfigFile
206 }
fdbda9e3 207
7c3b7976 208 const configOverride: any = {}
86ebdf8c 209
913b1d71 210 if (server.parallel) {
7c3b7976 211 Object.assign(configOverride, {
86ebdf8c 212 listen: {
913b1d71 213 port: server.port
86ebdf8c
C
214 },
215 webserver: {
913b1d71 216 port: server.port
86ebdf8c
C
217 },
218 database: {
913b1d71 219 suffix: '_test' + server.internalServerNumber
86ebdf8c
C
220 },
221 storage: {
913b1d71
C
222 tmp: `test${server.internalServerNumber}/tmp/`,
223 avatars: `test${server.internalServerNumber}/avatars/`,
224 videos: `test${server.internalServerNumber}/videos/`,
225 streaming_playlists: `test${server.internalServerNumber}/streaming-playlists/`,
226 redundancy: `test${server.internalServerNumber}/redundancy/`,
227 logs: `test${server.internalServerNumber}/logs/`,
228 previews: `test${server.internalServerNumber}/previews/`,
229 thumbnails: `test${server.internalServerNumber}/thumbnails/`,
230 torrents: `test${server.internalServerNumber}/torrents/`,
231 captions: `test${server.internalServerNumber}/captions/`,
89cd1275
C
232 cache: `test${server.internalServerNumber}/cache/`,
233 plugins: `test${server.internalServerNumber}/plugins/`
86ebdf8c
C
234 },
235 admin: {
913b1d71 236 email: `admin${server.internalServerNumber}@example.com`
c655c9ef
C
237 },
238 live: {
239 rtmp: {
240 port: server.rtmpPort
241 }
86ebdf8c 242 }
7c3b7976 243 })
86ebdf8c
C
244 }
245
246 if (configOverrideArg !== undefined) {
247 Object.assign(configOverride, configOverrideArg)
fdbda9e3
C
248 }
249
7c3b7976
C
250 // Share the environment
251 const env = Object.create(process.env)
252 env['NODE_ENV'] = 'test'
253 env['NODE_APP_INSTANCE'] = server.internalServerNumber.toString()
86ebdf8c
C
254 env['NODE_CONFIG'] = JSON.stringify(configOverride)
255
bd2e2f11 256 const forkOptions = {
0e1dc3e7 257 silent: true,
7c3b7976 258 env,
bd2e2f11
C
259 detached: true,
260 execArgv: options.execArgv || []
0e1dc3e7
C
261 }
262
263 return new Promise<ServerInfo>(res => {
bd2e2f11 264 server.app = fork(join(root(), 'dist', 'server.js'), args, forkOptions)
42e1ec25
C
265 server.app.stdout.on('data', function onStdout (data) {
266 let dontContinue = false
267
268 // Capture things if we want to
269 for (const key of Object.keys(regexps)) {
a1587156 270 const regexp = regexps[key]
42e1ec25
C
271 const matches = data.toString().match(regexp)
272 if (matches !== null) {
a1587156
C
273 if (key === 'client_id') server.client.id = matches[1]
274 else if (key === 'client_secret') server.client.secret = matches[1]
275 else if (key === 'user_username') server.user.username = matches[1]
276 else if (key === 'user_password') server.user.password = matches[1]
42e1ec25
C
277 }
278 }
279
280 // Check if all required sentences are here
281 for (const key of Object.keys(serverRunString)) {
a1587156
C
282 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
283 if (serverRunString[key] === false) dontContinue = true
42e1ec25
C
284 }
285
286 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
287 if (dontContinue === true) return
288
bd2e2f11 289 if (options.hideLogs === false) {
5a547f69
C
290 console.log(data.toString())
291 } else {
292 server.app.stdout.removeListener('data', onStdout)
293 }
42e1ec25
C
294
295 process.on('exit', () => {
296 try {
297 process.kill(server.app.pid)
298 } catch { /* empty */ }
dc094603 299 })
42e1ec25 300
329619b3
C
301 server.bulkCommand = new BulkCommand(server)
302 server.cliCommand = new CLICommand(server)
e8bd7ce7 303 server.customPageCommand = new CustomPagesCommand(server)
c1bc8ee4 304 server.feedCommand = new FeedCommand(server)
a92ddacb 305 server.logsCommand = new LogsCommand(server)
0c1a77e9 306 server.abusesCommand = new AbusesCommand(server)
23a3a882 307 server.overviewsCommand = new OverviewsCommand(server)
af971e06 308 server.searchCommand = new SearchCommand(server)
a9c58393 309 server.contactFormCommand = new ContactFormCommand(server)
883a9019 310 server.debugCommand = new DebugCommand(server)
c3d29f69 311 server.followsCommand = new FollowsCommand(server)
9c6327f8 312 server.jobsCommand = new JobsCommand(server)
ae2abfd3 313 server.pluginsCommand = new PluginsCommand(server)
dab04709 314 server.redundancyCommand = new RedundancyCommand(server)
bc809041 315 server.statsCommand = new StatsCommand(server)
65e6e260 316 server.configCommand = new ConfigCommand(server)
329619b3 317
42e1ec25
C
318 res(server)
319 })
0e1dc3e7
C
320 })
321}
322
e5565833 323async function reRunServer (server: ServerInfo, configOverride?: any) {
913b1d71 324 const newServer = await runServer(server, configOverride)
7bc29171
C
325 server.app = newServer.app
326
327 return server
328}
329
d1a2ce5e 330async function checkTmpIsEmpty (server: ServerInfo) {
f6d6e7f8 331 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
d1a2ce5e
C
332
333 if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
334 await checkDirectoryIsEmpty(server, 'tmp/hls')
335 }
09209296
C
336}
337
8d2be0ed 338async function checkDirectoryIsEmpty (server: ServerInfo, directory: string, exceptions: string[] = []) {
48f07b4a 339 const testDirectory = 'test' + server.internalServerNumber
89231874 340
09209296 341 const directoryPath = join(root(), testDirectory, directory)
89231874 342
8d2be0ed 343 const directoryExists = await pathExists(directoryPath)
89231874
C
344 expect(directoryExists).to.be.true
345
346 const files = await readdir(directoryPath)
8d2be0ed
C
347 const filtered = files.filter(f => exceptions.includes(f) === false)
348
349 expect(filtered).to.have.lengthOf(0)
89231874
C
350}
351
0e1dc3e7
C
352function killallServers (servers: ServerInfo[]) {
353 for (const server of servers) {
7c3b7976
C
354 if (!server.app) continue
355
0e1dc3e7 356 process.kill(-server.app.pid)
7c3b7976 357 server.app = null
0e1dc3e7
C
358 }
359}
360
83ef31fe 361async function cleanupTests (servers: ServerInfo[]) {
86ebdf8c
C
362 killallServers(servers)
363
83ef31fe
C
364 if (isGithubCI()) {
365 await ensureDir('artifacts')
366 }
367
86ebdf8c
C
368 const p: Promise<any>[] = []
369 for (const server of servers) {
83ef31fe
C
370 if (isGithubCI()) {
371 const origin = await buildServerDirectory(server, 'logs/peertube.log')
372 const destname = `peertube-${server.internalServerNumber}.log`
373 console.log('Saving logs %s.', destname)
374
375 await copy(origin, join('artifacts', destname))
376 }
377
86ebdf8c
C
378 if (server.parallel) {
379 p.push(flushTests(server.internalServerNumber))
380 }
7c3b7976
C
381
382 if (server.customConfigFile) {
383 p.push(remove(server.customConfigFile))
384 }
86ebdf8c
C
385 }
386
387 return Promise.all(p)
388}
389
1b05d82d 390async function waitUntilLog (server: ServerInfo, str: string, count = 1, strictCount = true) {
ca5c612b 391 const logfile = buildServerDirectory(server, 'logs/peertube.log')
792e5b8e
C
392
393 while (true) {
394 const buf = await readFile(logfile)
395
396 const matches = buf.toString().match(new RegExp(str, 'g'))
397 if (matches && matches.length === count) return
1b05d82d 398 if (matches && strictCount === false && matches.length >= count) return
792e5b8e
C
399
400 await wait(1000)
401 }
402}
403
d218e7de 404async function getServerFileSize (server: ServerInfo, subPath: string) {
ca5c612b 405 const path = buildServerDirectory(server, subPath)
d218e7de
C
406
407 return getFileSize(path)
408}
409
78d62f4d
C
410function makePingRequest (server: ServerInfo) {
411 return makeGetRequest({
412 url: server.url,
413 path: '/api/v1/ping',
414 statusCodeExpected: 200
415 })
416}
417
0e1dc3e7
C
418// ---------------------------------------------------------------------------
419
420export {
09209296 421 checkDirectoryIsEmpty,
89231874 422 checkTmpIsEmpty,
d218e7de 423 getServerFileSize,
0e1dc3e7 424 ServerInfo,
7c3b7976 425 parallelTests,
86ebdf8c 426 cleanupTests,
0e1dc3e7
C
427 flushAndRunMultipleServers,
428 flushTests,
78d62f4d 429 makePingRequest,
210feb6c 430 flushAndRunServer,
7bc29171 431 killallServers,
792e5b8e
C
432 reRunServer,
433 waitUntilLog
0e1dc3e7 434}