Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,27 @@ jobs:

runs-on: ubuntu-latest

strategy:
matrix:
java: [ 17, 21 ]

name: Java ${{ matrix.java }}

steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 11
cache: maven
- name: Build with Maven
run: ./resources/build.sh -Pcoverage
- name: Upload coverage report
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}
cache: maven

- name: Build with Maven
run: ./resources/build.sh -Pcoverage

- name: Upload coverage report
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
4 changes: 2 additions & 2 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ Maintainer: dev@jitsi.org
Section: net
Priority: optional
Standards-Version: 3.9.2
Build-Depends: openjdk-11-jdk | openjdk-17-jdk, debhelper (>= 9)
Build-Depends: openjdk-17-jdk | openjdk-21-jdk, debhelper (>= 9)

Package: jibri
Architecture: all
Depends: openjdk-11-jre-headless | openjdk-11-jre | openjdk-17-jre-headless | openjdk-17-jre, ffmpeg, curl, alsa-utils, icewm, xserver-xorg-video-dummy, procps, ruby-hocon
Depends: openjdk-17-jre-headless | openjdk-17-jre | openjdk-21-jre-headless | openjdk-21-jre, ffmpeg, curl, alsa-utils, icewm, xserver-xorg-video-dummy, procps, ruby-hocon
Description: Jibri
Jibri can be used to capture data from a Jitsi Meet conference and record it to a file or stream it to a url
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<argLine>
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
</argLine>
<systemPropertyVariables>
<!-- gradle.build.dir is the property name that JunitXmlReporter expects to find, even though
this is maven -->
Expand Down
5 changes: 2 additions & 3 deletions src/main/kotlin/org/jitsi/jibri/util/ProcessFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ class ProcessFactory {
fun createProcess(
command: List<String>,
parentLogger: Logger,
environment: Map<String, String> = mapOf(),
processBuilder: ProcessBuilder = ProcessBuilder()
environment: Map<String, String> = mapOf()
): ProcessWrapper {
return ProcessWrapper(command, parentLogger, environment, processBuilder)
return ProcessWrapper(command, parentLogger, environment)
}
}
38 changes: 20 additions & 18 deletions src/main/kotlin/org/jitsi/jibri/util/ProcessWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import java.io.InputStream
import java.time.Duration
import java.util.concurrent.TimeUnit

typealias ProcessStopper = (pid: Long) -> Unit

/**
* A wrapper around [Process] that implements
* behaviors more useful to Jibri. This isn't done
Expand All @@ -35,28 +37,35 @@ class ProcessWrapper(
command: List<String>,
parentLogger: Logger,
val environment: Map<String, String> = mapOf(),
private val processBuilder: ProcessBuilder = ProcessBuilder(),
private val runtime: Runtime = Runtime.getRuntime()
private val processStarter: () -> Process = {
ProcessBuilder().apply {
command(command)
redirectErrorStream(true)
environment().putAll(environment)
}.start()
},
private val stopper: ProcessStopper = { pid ->
Runtime.getRuntime().exec(
arrayOf("kill", "-s", "SIGINT", pid.toString())
)
}
) {
private val logger = createChildLogger(parentLogger)

/**
* The actual underlying [Process] this wrapper
* wraps
* The actual underlying [Process] this wrapper wraps
*/
private lateinit var process: Process

/**
* A 'Tee' which allows us to split the stdout
* stream coming from the process into multiple
* independent streams which can be read from
* independently
* independent streams which can be read from independently
*/
private lateinit var tee: Tee

/**
* Observes the most recent line of output from
* the wrapped process
* Observes the most recent line of output from the wrapped process
*/
private lateinit var tail: Tail

Expand All @@ -74,18 +83,11 @@ class ProcessWrapper(
val exitValue: Int
get() = process.exitValue()

init {
processBuilder.command(command)
processBuilder.redirectErrorStream(true)
processBuilder.environment().putAll(environment)
}

/**
* Starts this Process. Will throw if there's an error
* (see [ProcessBuilder.start])
* Starts this Process. Will throw if there's an error.
*/
fun start() {
process = processBuilder.start()
process = processStarter()
tee = Tee(process.inputStream)
tail = Tail(getOutput())
}
Expand All @@ -99,7 +101,7 @@ class ProcessWrapper(
// because we want them to read everything available from the
// process' inputstream. Once it's done, they'll read
// the EOF and close things up correctly
runtime.exec("kill -s SIGINT ${process.pid()}")
stopper(process.pid())
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ internal class FileRecordingJibriServiceTest : ShouldSpec() {
every { seleniumMockHelper.mock.getParticipants() } returns listOf(mapOf("a" to "b"))
val finalizeProcessMock = createFinalizeProcessMock(true)
every {
processFactory.createProcess(match { it.first().contains("finalize") }, any(), any(), any())
processFactory.createProcess(match { it.first().contains("finalize") }, any(), any())
} returns finalizeProcessMock

fileRecordingJibriService.stop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ internal class SipGatewayJibriServiceTest : ShouldSpec() {
// Stop the service
val finalizeProcessMock = createFinalizeProcessMock(true)
every {
processFactory.createProcess(eq(listOf("/opt/jitsi/jibri/finalize_sip.sh")), any(), any(), any())
processFactory.createProcess(eq(listOf("/opt/jitsi/jibri/finalize_sip.sh")), any(), any())
} returns finalizeProcessMock

sipGatewayJibriService.stop()
Expand All @@ -165,7 +165,7 @@ internal class SipGatewayJibriServiceTest : ShouldSpec() {
// Stop the service
val finalizeProcessMock = createFinalizeProcessMock(false)
every {
processFactory.createProcess(eq(listOf("/opt/jitsi/jibri/finalize_sip.sh")), any(), any(), any())
processFactory.createProcess(eq(listOf("/opt/jitsi/jibri/finalize_sip.sh")), any(), any())
} returns finalizeProcessMock

sipGatewayJibriService.stop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal class JibriSubprocessTest : ShouldSpec() {
}
}

every { processFactory.createProcess(any(), any(), any(), any()) } returns processWrapper
every { processFactory.createProcess(any(), any(), any()) } returns processWrapper
every { processStatePublisher.addStatusHandler(capture(processStateHandler)) } just Runs

subprocess.addStatusHandler { status ->
Expand Down
55 changes: 29 additions & 26 deletions src/test/kotlin/org/jitsi/jibri/util/ProcessWrapperTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,13 @@ import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import org.jitsi.jibri.helpers.seconds
import org.jitsi.jibri.helpers.within
import org.jitsi.utils.logging2.Logger
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.io.PipedInputStream
import java.io.PipedOutputStream
Expand All @@ -41,9 +38,10 @@ import java.util.concurrent.TimeUnit
internal class ProcessWrapperTest : ShouldSpec() {
override fun isolationMode(): IsolationMode? = IsolationMode.InstancePerLeaf

private val processBuilder: ProcessBuilder = mockk(relaxed = true)
private val process: Process = mockk(relaxed = true)
private val runtime: Runtime = mockk(relaxed = true)
private var stoppedPid: Long = -1
private lateinit var stopperException: Throwable
private var stopperThrows = false
private lateinit var outputStream: PipedOutputStream
private lateinit var inputStream: PipedInputStream
private lateinit var processWrapper: ProcessWrapper
Expand All @@ -53,17 +51,22 @@ internal class ProcessWrapperTest : ShouldSpec() {
beforeTest {
outputStream = PipedOutputStream()
inputStream = PipedInputStream(outputStream)
stopperThrows = false

every { process.inputStream } returns inputStream
every { process.destroyForcibly() } returns process
every { processBuilder.command(any<List<String>>()) } returns processBuilder
every { processBuilder.start() } returns process
every { process.pid() } returns 12345L

val stopper: ProcessStopper = { pid ->
if (stopperThrows) throw stopperException
stoppedPid = pid
}

processWrapper = ProcessWrapper(
listOf(),
parentLogger = logger,
processBuilder = processBuilder,
runtime = runtime
processStarter = { process },
stopper = stopper
)
processWrapper.start()
}
Expand Down Expand Up @@ -143,42 +146,42 @@ internal class ProcessWrapperTest : ShouldSpec() {
}
}
context("stop") {
should("invoke the correct command") {
val execCaptor = slot<String>()
every { runtime.exec(capture(execCaptor)) } returns process
should("invoke the stopper with the process pid") {
processWrapper.stop()
execCaptor.captured shouldContain "kill -s SIGINT"
stoppedPid shouldBe 12345L
}
context("when the runtime throws IOException") {
every { runtime.exec(any<String>()) } throws IOException()
context("when the stopper throws IOException") {
should("let the exception bubble up") {
shouldThrow<IOException> { processWrapper.stop() }
stopperThrows = true
stopperException = java.io.IOException()
shouldThrow<java.io.IOException> { processWrapper.stop() }
}
}
context("when the runtime throws RuntimeException") {
every { runtime.exec(any<String>()) } throws RuntimeException()
context("when the stopper throws RuntimeException") {
should("let the exception bubble up") {
stopperThrows = true
stopperException = RuntimeException()
shouldThrow<RuntimeException> { processWrapper.stop() }
}
}
}
context("stopAndWaitFor") {
should("invoke the correct command") {
val execCaptor = slot<String>()
every { runtime.exec(capture(execCaptor)) } returns process
should("invoke the stopper and wait") {
every { process.waitFor(any(), any()) } returns true
processWrapper.stopAndWaitFor(Duration.ofSeconds(10)) shouldBe true
execCaptor.captured shouldContain "kill -s SIGINT"
stoppedPid shouldBe 12345L
}
context("when the runtime throws IOException") {
every { runtime.exec(any<String>()) } throws IOException()
context("when the stopper throws IOException") {
should("handle it correctly") {
stopperThrows = true
stopperException = java.io.IOException()
processWrapper.stopAndWaitFor(Duration.ofSeconds(10)) shouldBe false
}
}
context("when the runtime throws RuntimeException") {
every { runtime.exec(any<String>()) } throws RuntimeException()
context("when the stopper throws RuntimeException") {
should("handle it correctly") {
stopperThrows = true
stopperException = RuntimeException()
processWrapper.stopAndWaitFor(Duration.ofSeconds(10)) shouldBe false
}
}
Expand Down
Loading