Skip to content

Commit faee555

Browse files
committed
fix(file-io): Do not leak SentryFileInputStream/SentryFileOutputStream descriptors and channels
1 parent 5b39391 commit faee555

File tree

6 files changed

+85
-14
lines changed

6 files changed

+85
-14
lines changed

sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,15 @@
1414
import io.sentry.protocol.User;
1515
import io.sentry.samples.android.compose.ComposeActivity;
1616
import io.sentry.samples.android.databinding.ActivityMainBinding;
17-
import java.io.BufferedWriter;
1817
import java.io.File;
1918
import java.io.FileOutputStream;
2019
import java.io.IOException;
2120
import java.io.InputStream;
22-
import java.io.OutputStreamWriter;
23-
import java.io.Writer;
21+
import java.nio.channels.FileChannel;
2422
import java.util.ArrayList;
2523
import java.util.Calendar;
2624
import java.util.Collections;
2725
import java.util.List;
28-
import java.util.Locale;
2926
import java.util.concurrent.CountDownLatch;
3027
import timber.log.Timber;
3128

@@ -86,16 +83,10 @@ protected void onCreate(Bundle savedInstanceState) {
8683
view -> {
8784
String fileName = Calendar.getInstance().getTimeInMillis() + "_file.txt";
8885
File file = getApplication().getFileStreamPath(fileName);
89-
try (final FileOutputStream fileOutputStream = new SentryFileOutputStream(file);
90-
final OutputStreamWriter outputStreamWriter =
91-
new OutputStreamWriter(fileOutputStream);
92-
final Writer writer = new BufferedWriter(outputStreamWriter)) {
93-
for (int i = 0; i < 1024; i++) {
94-
// To keep the sample code simple this happens on the main thread. Don't do this in a
95-
// real app.
96-
writer.write(String.format(Locale.getDefault(), "%d\n", i));
97-
}
98-
writer.flush();
86+
try (final FileOutputStream fis =
87+
SentryFileOutputStream.Factory.create(new FileOutputStream(file), file)) {
88+
FileChannel channel = fis.getChannel();
89+
channel.write(java.nio.ByteBuffer.wrap("Hello, World!".getBytes()));
9990
} catch (IOException e) {
10091
Sentry.captureException(e);
10192
}

sentry/src/main/java/io/sentry/instrumentation/file/SentryFileInputStream.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public long skip(final long n) throws IOException {
113113
@Override
114114
public void close() throws IOException {
115115
spanManager.finish(delegate);
116+
super.close();
116117
}
117118

118119
private static FileDescriptor getFileDescriptor(final @NotNull FileInputStream stream)

sentry/src/main/java/io/sentry/instrumentation/file/SentryFileOutputStream.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ public void write(final byte @NotNull [] b, final int off, final int len) throws
120120
@Override
121121
public void close() throws IOException {
122122
spanManager.finish(delegate);
123+
super.close();
123124
}
124125

125126
private static FileDescriptor getFileDescriptor(final @NotNull FileOutputStream stream)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.sentry
2+
3+
import io.sentry.cache.PersistingScopeObserver
4+
import org.junit.Rule
5+
import org.junit.Test
6+
import org.junit.rules.TemporaryFolder
7+
8+
class PersistingScopeObserverPerformanceTest {
9+
10+
@get:Rule
11+
val tmpDir = TemporaryFolder()
12+
13+
class Fixture {
14+
15+
val options = SentryOptions()
16+
val scope = Scope(options)
17+
18+
fun getSut(cacheDir: TemporaryFolder): PersistingScopeObserver {
19+
options.run {
20+
executorService = SentryExecutorService()
21+
cacheDirPath = cacheDir.newFolder().absolutePath
22+
}
23+
return PersistingScopeObserver(options)
24+
}
25+
}
26+
27+
private val fixture = Fixture()
28+
29+
@Test
30+
fun test() {
31+
val sut = fixture.getSut(tmpDir)
32+
33+
var oldDuration = 0L
34+
var newDuration = 0L
35+
36+
val crumb = Breadcrumb.http("https://example.com", "GET", 200)
37+
38+
for (i in 0..100) {
39+
val oldStart = System.currentTimeMillis()
40+
for (j in 0..100) {
41+
sut.addBreadcrumb(crumb)
42+
}
43+
oldDuration += System.currentTimeMillis() - oldStart
44+
45+
val newStart = System.currentTimeMillis()
46+
for (j in 0..100) {
47+
// sut.addBreadcrumbNew(crumb)
48+
}
49+
newDuration += System.currentTimeMillis() - newStart
50+
}
51+
52+
println("It took old: ${oldDuration}ms, new: ${newDuration}ms")
53+
}
54+
}

sentry/src/test/java/io/sentry/instrumentation/file/SentryFileInputStreamTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import java.io.File
1818
import java.io.FileDescriptor
1919
import java.io.FileInputStream
2020
import java.io.IOException
21+
import java.nio.ByteBuffer
2122
import java.util.concurrent.atomic.AtomicBoolean
2223
import kotlin.concurrent.thread
2324
import kotlin.test.Test
@@ -285,6 +286,17 @@ class SentryFileInputStreamTest {
285286

286287
assertTrue { stream is ThrowingFileInputStream }
287288
}
289+
290+
@Test
291+
fun `channels and descriptors are closed together with the stream`() {
292+
val fis = fixture.getSut(tmpFile)
293+
val channel = fis.channel
294+
295+
channel.read(ByteBuffer.allocate(1))
296+
fis.close()
297+
assertFalse(channel.isOpen)
298+
assertFalse(fis.fd.valid())
299+
}
288300
}
289301

290302
class ThrowingFileInputStream(file: File) : FileInputStream(file) {

sentry/src/test/java/io/sentry/instrumentation/file/SentryFileOutputStreamTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.mockito.kotlin.whenever
1616
import java.io.File
1717
import java.io.FileOutputStream
1818
import java.io.IOException
19+
import java.nio.ByteBuffer
1920
import java.util.concurrent.atomic.AtomicBoolean
2021
import kotlin.concurrent.thread
2122
import kotlin.test.Test
@@ -231,6 +232,17 @@ class SentryFileOutputStreamTest {
231232

232233
assertTrue { stream is ThrowingFileOutputStream }
233234
}
235+
236+
@Test
237+
fun `channels and descriptors are closed together with the stream`() {
238+
val fos = fixture.getSut(tmpFile)
239+
val channel = fos.channel
240+
241+
channel.write(ByteBuffer.wrap("hello".toByteArray()))
242+
fos.close()
243+
assertFalse(channel.isOpen)
244+
assertFalse(fos.fd.valid())
245+
}
234246
}
235247

236248
class ThrowingFileOutputStream(file: File) : FileOutputStream(file) {

0 commit comments

Comments
 (0)