Skip to content

Commit d016dd5

Browse files
trgpaartembilan
authored andcommitted
GH-3507: Fix Tail producer for proper command
Fixes #3507 The `OSDelegatingFileTailingMessageProducer` passing command string to `Runtime.getRuntime().exec()` may cause problems if spaces (and other special characters) are used in the filename. * Use an array for command and its options to let the target `Runtime` to parse and execute it properly **Cherry-pick to 5.4.x, 5.3.x & 5.2.x** # Conflicts: # spring-integration-file/src/main/java/org/springframework/integration/file/tail/OSDelegatingFileTailingMessageProducer.java
1 parent b7d9b87 commit d016dd5

File tree

3 files changed

+87
-39
lines changed

3 files changed

+87
-39
lines changed

spring-integration-file/src/main/java/org/springframework/integration/file/tail/OSDelegatingFileTailingMessageProducer.java

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,22 +33,28 @@
3333
* @author Gary Russell
3434
* @author Gavin Gray
3535
* @author Ali Shahbour
36+
* @author Artem Bilan
37+
* @author Trung Pham
38+
*
3639
* @since 3.0
3740
*
3841
*/
3942
public class OSDelegatingFileTailingMessageProducer extends FileTailingMessageProducerSupport
4043
implements SchedulingAwareRunnable {
4144

42-
private volatile Process nativeTailProcess;
4345

4446
private volatile String options = "-F -n 0";
4547

4648
private volatile String command = "ADAPTER_NOT_INITIALIZED";
4749

48-
private volatile boolean enableStatusReader = true;
50+
private volatile String[] tailCommand;
51+
52+
private volatile Process nativeTailProcess;
4953

5054
private volatile BufferedReader stdOutReader;
5155

56+
private volatile boolean enableStatusReader = true;
57+
5258
public void setOptions(String options) {
5359
if (options == null) {
5460
this.options = "";
@@ -92,8 +98,13 @@ protected void onInit() {
9298
protected void doStart() {
9399
super.doStart();
94100
destroyProcess();
95-
this.command = "tail " + this.options + " " + this.getFile().getAbsolutePath();
96-
this.getTaskExecutor().execute(this::runExec);
101+
String[] tailOptions = this.options.split("\\s+");
102+
this.tailCommand = new String[tailOptions.length + 2];
103+
this.tailCommand[0] = "tail";
104+
this.tailCommand[this.tailCommand.length - 1] = getFile().getAbsolutePath();
105+
System.arraycopy(tailOptions, 0, this.tailCommand, 1, tailOptions.length);
106+
this.command = String.join(" ", this.tailCommand);
107+
getTaskExecutor().execute(this::runExec);
97108
}
98109

99110
@Override
@@ -114,20 +125,18 @@ private void destroyProcess() {
114125
* Exec the native tail process.
115126
*/
116127
private void runExec() {
117-
this.destroyProcess();
118-
if (logger.isInfoEnabled()) {
119-
logger.info("Starting tail process");
120-
}
128+
destroyProcess();
129+
logger.info("Starting tail process");
121130
try {
122-
Process process = Runtime.getRuntime().exec(this.command);
131+
Process process = Runtime.getRuntime().exec(this.tailCommand);
123132
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
124133
this.nativeTailProcess = process;
125-
this.startProcessMonitor();
134+
startProcessMonitor();
126135
if (this.enableStatusReader) {
127136
startStatusReader();
128137
}
129138
this.stdOutReader = reader;
130-
this.getTaskExecutor().execute(this);
139+
getTaskExecutor().execute(this);
131140
}
132141
catch (IOException e) {
133142
throw new MessagingException("Failed to exec tail command: '" + this.command + "'", e);
@@ -139,14 +148,13 @@ private void runExec() {
139148
* Runs a thread that waits for the Process result.
140149
*/
141150
private void startProcessMonitor() {
142-
this.getTaskExecutor().execute(() -> {
143-
Process process = OSDelegatingFileTailingMessageProducer.this.nativeTailProcess;
144-
if (process == null) {
145-
if (logger.isDebugEnabled()) {
146-
logger.debug("Process destroyed before starting process monitor");
147-
}
148-
return;
149-
}
151+
getTaskExecutor()
152+
.execute(() -> {
153+
Process process = OSDelegatingFileTailingMessageProducer.this.nativeTailProcess;
154+
if (process == null) {
155+
logger.debug("Process destroyed before starting process monitor");
156+
return;
157+
}
150158

151159
int result = Integer.MIN_VALUE;
152160
try {

spring-integration-file/src/test/java/org/springframework/integration/file/tail/FileTailingMessageProducerTests.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -53,6 +53,7 @@
5353
* @author Gavin Gray
5454
* @author Artem Bilan
5555
* @author Ali Shahbour
56+
* @author Trung Pham
5657
*
5758
* @since 3.0
5859
*/
@@ -183,7 +184,7 @@ private void testGuts(FileTailingMessageProducerSupport adapter, String field)
183184
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
184185
taskScheduler.afterPropertiesSet();
185186
adapter.setTaskScheduler(taskScheduler);
186-
final List<FileTailingEvent> events = new ArrayList<FileTailingEvent>();
187+
final List<FileTailingEvent> events = new ArrayList<>();
187188
adapter.setApplicationEventPublisher(event -> {
188189
FileTailingEvent tailEvent = (FileTailingEvent) event;
189190
logger.debug(event);
@@ -232,6 +233,8 @@ private void testGuts(FileTailingMessageProducerSupport adapter, String field)
232233
}
233234

234235
assertThat(events.size()).isGreaterThanOrEqualTo(1);
236+
237+
taskScheduler.destroy();
235238
}
236239

237240
private void waitForField(FileTailingMessageProducerSupport adapter, String field) throws Exception {
@@ -248,4 +251,38 @@ private void waitForField(FileTailingMessageProducerSupport adapter, String fiel
248251
fail("adapter failed to start");
249252
}
250253

254+
@Test
255+
@TailAvailable
256+
public void canHandleFilenameHavingSpecialCharacters() throws Exception {
257+
File file = File.createTempFile("foo bar", " -c 1");
258+
file.delete();
259+
260+
OSDelegatingFileTailingMessageProducer adapter = new OSDelegatingFileTailingMessageProducer();
261+
adapter.setOptions(TAIL_OPTIONS_FOLLOW_NAME_ALL_LINES);
262+
adapter.setFile(file);
263+
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
264+
taskScheduler.afterPropertiesSet();
265+
adapter.setTaskScheduler(taskScheduler);
266+
QueueChannel outputChannel = new QueueChannel();
267+
adapter.setOutputChannel(outputChannel);
268+
adapter.setTailAttemptsDelay(500);
269+
adapter.setBeanFactory(mock(BeanFactory.class));
270+
adapter.afterPropertiesSet();
271+
272+
adapter.start();
273+
waitForField(adapter, "stdOutReader");
274+
275+
FileOutputStream fos = new FileOutputStream(file);
276+
fos.write(("hello foobar\n").getBytes());
277+
fos.close();
278+
279+
Message<?> message = outputChannel.receive(10000);
280+
assertThat(message).as("expected a non-null message").isNotNull();
281+
assertThat(message.getPayload()).isEqualTo("hello foobar");
282+
283+
adapter.stop();
284+
file.delete();
285+
taskScheduler.destroy();
286+
}
287+
251288
}

spring-integration-file/src/test/java/org/springframework/integration/file/tail/TailRule.java

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
import java.io.OutputStream;
2525
import java.util.concurrent.CountDownLatch;
2626
import java.util.concurrent.ExecutionException;
27+
import java.util.concurrent.ExecutorService;
2728
import java.util.concurrent.Executors;
2829
import java.util.concurrent.Future;
2930
import java.util.concurrent.TimeUnit;
@@ -75,9 +76,6 @@ public void evaluate() {
7576
}
7677

7778
private boolean tailWorksOnThisMachine() {
78-
if (tmpDir.contains(":")) {
79-
return false;
80-
}
8179
File testDir = new File(tmpDir, "FileTailingMessageProducerTests");
8280
testDir.mkdir();
8381
final File file = new File(testDir, "foo");
@@ -88,19 +86,24 @@ private boolean tailWorksOnThisMachine() {
8886
fos.close();
8987
final AtomicReference<Integer> c = new AtomicReference<>();
9088
final CountDownLatch latch = new CountDownLatch(1);
91-
Future<Process> future = Executors.newSingleThreadExecutor().submit(() -> {
92-
final Process process = Runtime.getRuntime().exec(commandToTest + " " + file.getAbsolutePath());
93-
Executors.newSingleThreadExecutor().execute(() -> {
94-
try {
95-
c.set(process.getInputStream().read());
96-
latch.countDown();
97-
}
98-
catch (IOException e) {
99-
logger.error("Error reading test stream", e);
100-
}
101-
});
102-
return process;
103-
});
89+
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
90+
Future<Process> future =
91+
newSingleThreadExecutor.submit(() -> {
92+
final Process process = Runtime.getRuntime().exec(commandToTest + " " + file.getAbsolutePath());
93+
ExecutorService executorService = Executors.newSingleThreadExecutor();
94+
executorService.execute(() -> {
95+
try {
96+
c.set(process.getInputStream().read());
97+
latch.countDown();
98+
}
99+
catch (IOException e) {
100+
logger.error("Error reading test stream", e);
101+
}
102+
});
103+
executorService.shutdown();
104+
return process;
105+
});
106+
newSingleThreadExecutor.shutdown();
104107
try {
105108
Process process = future.get(10, TimeUnit.SECONDS);
106109
if (latch.await(10, TimeUnit.SECONDS)) {

0 commit comments

Comments
 (0)