1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.helper;
17
18 import java.io.IOException;
19 import java.io.OutputStream;
20 import java.util.List;
21 import java.util.Set;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.CountDownLatch;
24 import java.util.concurrent.TimeUnit;
25 import java.util.function.Consumer;
26
27 import org.apache.commons.io.IOUtils;
28 import org.apache.logging.log4j.LogManager;
29 import org.apache.logging.log4j.Logger;
30 import org.codelibs.core.io.CloseableUtil;
31 import org.codelibs.fess.Constants;
32 import org.codelibs.fess.exception.JobNotFoundException;
33 import org.codelibs.fess.exception.JobProcessingException;
34 import org.codelibs.fess.util.InputStreamThread;
35 import org.codelibs.fess.util.JobProcess;
36
37 import jakarta.annotation.PreDestroy;
38
39
40
41
42
43
44 public class ProcessHelper {
45
46 private static final Logger logger = LogManager.getLogger(ProcessHelper.class);
47
48
49 protected final ConcurrentHashMap<String, JobProcess> runningProcessMap = new ConcurrentHashMap<>();
50
51
52 protected int processDestroyTimeout = 10;
53
54
55 protected int streamCloseTimeout = 10;
56
57
58
59
60
61 public ProcessHelper() {
62
63 }
64
65
66
67
68
69 @PreDestroy
70 public void destroy() {
71 for (final String sessionId : runningProcessMap.keySet()) {
72 if (logger.isInfoEnabled()) {
73 logger.info("Stopping process {}", sessionId);
74 }
75 if (destroyProcess(sessionId) == 0 && logger.isInfoEnabled()) {
76 logger.info("Stopped process {}", sessionId);
77 }
78 }
79 }
80
81
82
83
84
85
86
87
88
89
90 public JobProcess startProcess(final String sessionId, final List<String> cmdList, final Consumer<ProcessBuilder> pbCall) {
91 return startProcess(sessionId, cmdList, pbCall, InputStreamThread.MAX_BUFFER_SIZE, null);
92 }
93
94
95
96
97
98
99
100
101
102
103
104
105
106 public synchronized JobProcess startProcess(final String sessionId, final List<String> cmdList, final Consumer<ProcessBuilder> pbCall,
107 final int bufferSize, final Consumer<String> outputCallback) {
108 final ProcessBuilder pb = new ProcessBuilder(cmdList);
109 pbCall.accept(pb);
110
111
112 final JobProcess oldProcess = runningProcessMap.remove(sessionId);
113 if (oldProcess != null) {
114 destroyProcess(sessionId, oldProcess);
115 }
116
117
118 try {
119 final JobProcess jobProcess = new JobProcess(pb.start(), bufferSize, outputCallback);
120 runningProcessMap.put(sessionId, jobProcess);
121 return jobProcess;
122 } catch (final IOException e) {
123 throw new JobProcessingException("Crawler Process terminated.", e);
124 }
125 }
126
127
128
129
130
131
132
133 public synchronized int destroyProcess(final String sessionId) {
134 final JobProcess jobProcess = runningProcessMap.remove(sessionId);
135 return destroyProcess(sessionId, jobProcess);
136 }
137
138
139
140
141
142
143 public boolean isProcessRunning() {
144 return !runningProcessMap.isEmpty();
145 }
146
147
148
149
150
151
152
153 public boolean isProcessRunning(final String sessionId) {
154 final JobProcess jobProcess = runningProcessMap.get(sessionId);
155 return jobProcess != null && jobProcess.getProcess().isAlive();
156 }
157
158
159
160
161
162
163
164
165
166 protected int destroyProcess(final String sessionId, final JobProcess jobProcess) {
167 if (jobProcess != null) {
168 final InputStreamThread ist = jobProcess.getInputStreamThread();
169 try {
170 ist.interrupt();
171 } catch (final Exception e) {
172 logger.warn("Could not interrupt a thread of an input stream.", e);
173 }
174
175 final CountDownLatch latch = new CountDownLatch(3);
176 final Process process = jobProcess.getProcess();
177 new Thread(() -> {
178 try {
179 CloseableUtil.closeQuietly(process.getInputStream());
180 } catch (final Exception e) {
181 logger.warn("Could not close a process input stream.", e);
182 } finally {
183 latch.countDown();
184 }
185 }, "ProcessCloser-input-" + sessionId).start();
186 new Thread(() -> {
187 try {
188 CloseableUtil.closeQuietly(process.getErrorStream());
189 } catch (final Exception e) {
190 logger.warn("Could not close a process error stream.", e);
191 } finally {
192 latch.countDown();
193 }
194 }, "ProcessCloser-error-" + sessionId).start();
195 new Thread(() -> {
196 try {
197 CloseableUtil.closeQuietly(process.getOutputStream());
198 } catch (final Exception e) {
199 logger.warn("Could not close a process output stream.", e);
200 } finally {
201 latch.countDown();
202 }
203 }, "ProcessCloser-output-" + sessionId).start();
204
205 try {
206 latch.await(streamCloseTimeout, TimeUnit.SECONDS);
207 } catch (final InterruptedException e) {
208 logger.warn("Interrupted to wait a process.", e);
209 }
210 try {
211 process.destroyForcibly().waitFor(processDestroyTimeout, TimeUnit.SECONDS);
212 return process.exitValue();
213 } catch (final Exception e) {
214 logger.error("Could not destroy a process correctly.", e);
215 }
216 }
217 return -1;
218 }
219
220
221
222
223
224
225 public Set<String> getRunningSessionIdSet() {
226 return runningProcessMap.keySet();
227 }
228
229
230
231
232
233
234 public void setProcessDestroyTimeout(final int processDestroyTimeout) {
235 this.processDestroyTimeout = processDestroyTimeout;
236 }
237
238
239
240
241
242
243 public void setStreamCloseTimeout(final int streamCloseTimeout) {
244 this.streamCloseTimeout = streamCloseTimeout;
245 }
246
247
248
249
250
251
252
253
254
255
256 public void sendCommand(final String sessionId, final String command) {
257 final Process process;
258 synchronized (this) {
259 final JobProcess jobProcess = runningProcessMap.get(sessionId);
260 if (jobProcess == null) {
261 throw new JobNotFoundException("Job for " + sessionId + " is not found.");
262 }
263 process = jobProcess.getProcess();
264 if (process == null || !process.isAlive()) {
265 throw new JobNotFoundException("Process for " + sessionId + " is not running.");
266 }
267 }
268
269
270 try {
271 final OutputStream out = process.getOutputStream();
272 IOUtils.write(command + "\n", out, Constants.CHARSET_UTF_8);
273 out.flush();
274 } catch (final IOException e) {
275 throw new JobProcessingException(e);
276 }
277 }
278 }