Skip to content

Commit 9bb6621

Browse files
Merge pull request #213 from rjeberhard/exec-waitfor
Exec improvements to read non-zero exit code and implement waitFor(long, TimeUnit)
2 parents 095f00c + d3b0267 commit 9bb6621

File tree

5 files changed

+320
-82
lines changed

5 files changed

+320
-82
lines changed

examples/src/main/java/io/kubernetes/client/examples/ExecExample.java

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2017 The Kubernetes Authors.
2+
Copyright 2017, 2018 The Kubernetes Authors.
33
Licensed under the Apache License, Version 2.0 (the "License");
44
you may not use this file except in compliance with the License.
55
You may obtain a copy of the License at
@@ -19,6 +19,8 @@
1919
import io.kubernetes.client.Exec;
2020
import io.kubernetes.client.util.Config;
2121
import java.io.IOException;
22+
import java.util.ArrayList;
23+
import java.util.List;
2224

2325
/**
2426
* A simple example of how to use the Java API
@@ -30,17 +32,36 @@
3032
*/
3133
public class ExecExample {
3234
public static void main(String[] args) throws IOException, ApiException, InterruptedException {
35+
String podName = "nginx-4217019353-k5sn9";
36+
String namespace = "default";
37+
List<String> commands = new ArrayList<>();
38+
39+
int len = args.length;
40+
if (len >= 1) podName = args[0];
41+
if (len >= 2) namespace = args[1];
42+
for (int i = 2; i < len; i++) {
43+
commands.add(args[i]);
44+
}
45+
3346
ApiClient client = Config.defaultClient();
3447
Configuration.setDefaultApiClient(client);
3548

3649
Exec exec = new Exec();
3750
boolean tty = System.console() != null;
38-
// final Process proc = exec.exec("default", "nginx-2371676037-czqx3", new String[]
39-
// {"sh", "-c", "echo foo"}, true, tty);
51+
// final Process proc = exec.exec("default", "nginx-4217019353-k5sn9", new String[]
52+
// {"sh", "-c", "echo foo"}, true, tty);
4053
final Process proc =
41-
exec.exec("default", "nginx-4217019353-k5sn9", new String[] {"sh"}, true, tty);
54+
exec.exec(
55+
namespace,
56+
podName,
57+
commands.isEmpty()
58+
? new String[] {"sh"}
59+
: commands.toArray(new String[commands.size()]),
60+
true,
61+
tty);
4262

43-
new Thread(
63+
Thread in =
64+
new Thread(
4465
new Runnable() {
4566
public void run() {
4667
try {
@@ -49,10 +70,11 @@ public void run() {
4970
ex.printStackTrace();
5071
}
5172
}
52-
})
53-
.start();
73+
});
74+
in.start();
5475

55-
new Thread(
76+
Thread out =
77+
new Thread(
5678
new Runnable() {
5779
public void run() {
5880
try {
@@ -61,19 +83,16 @@ public void run() {
6183
ex.printStackTrace();
6284
}
6385
}
64-
})
65-
.start();
86+
});
87+
out.start();
6688

6789
proc.waitFor();
68-
try {
69-
// Wait for buffers to flush.
70-
Thread.sleep(2000);
71-
} catch (InterruptedException ex) {
72-
ex.printStackTrace();
73-
}
90+
91+
// wait for any last output; no need to wait for input thread
92+
out.join();
7493

7594
proc.destroy();
7695

77-
System.exit(0);
96+
System.exit(proc.exitValue());
7897
}
7998
}

util/src/main/java/io/kubernetes/client/Exec.java

Lines changed: 129 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2017 The Kubernetes Authors.
2+
Copyright 2017, 2018 The Kubernetes Authors.
33
Licensed under the Apache License, Version 2.0 (the "License");
44
you may not use this file except in compliance with the License.
55
You may obtain a copy of the License at
@@ -12,15 +12,31 @@
1212
*/
1313
package io.kubernetes.client;
1414

15+
import com.google.common.io.CharStreams;
16+
import com.google.gson.reflect.TypeToken;
1517
import io.kubernetes.client.models.V1Pod;
18+
import io.kubernetes.client.models.V1Status;
19+
import io.kubernetes.client.models.V1StatusCause;
20+
import io.kubernetes.client.models.V1StatusDetails;
1621
import io.kubernetes.client.util.WebSocketStreamHandler;
1722
import io.kubernetes.client.util.WebSockets;
1823
import java.io.IOException;
1924
import java.io.InputStream;
25+
import java.io.InputStreamReader;
2026
import java.io.OutputStream;
27+
import java.io.Reader;
28+
import java.lang.reflect.Type;
29+
import java.util.HashMap;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.concurrent.TimeUnit;
2133
import org.apache.commons.lang3.StringUtils;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
2236

2337
public class Exec {
38+
private static final Logger log = LoggerFactory.getLogger(Exec.class);
39+
2440
private ApiClient apiClient;
2541

2642
/** Simple Exec API constructor, uses default configuration */
@@ -170,20 +186,90 @@ public Process exec(
170186
throws ApiException, IOException {
171187
String path = makePath(namespace, name, command, container, stdin, tty);
172188

173-
WebSocketStreamHandler handler = new WebSocketStreamHandler();
174-
ExecProcess exec = new ExecProcess(handler);
189+
ExecProcess exec = new ExecProcess();
190+
WebSocketStreamHandler handler = exec.getHandler();
175191
WebSockets.stream(path, "GET", apiClient, handler);
176192

177193
return exec;
178194
}
179195

180-
private static class ExecProcess extends Process {
181-
WebSocketStreamHandler streamHandler;
182-
private int statusCode;
196+
static int parseExitCode(ApiClient client, InputStream inputStream) {
197+
try {
198+
Type returnType = new TypeToken<V1Status>() {}.getType();
199+
String body;
200+
try (final Reader reader = new InputStreamReader(inputStream)) {
201+
body = CharStreams.toString(reader);
202+
}
203+
204+
V1Status status = client.getJSON().deserialize(body, returnType);
205+
if ("Success".equals(status.getStatus())) return 0;
206+
207+
if ("NonZeroExitCode".equals(status.getReason())) {
208+
V1StatusDetails details = status.getDetails();
209+
if (details != null) {
210+
List<V1StatusCause> causes = details.getCauses();
211+
if (causes != null) {
212+
for (V1StatusCause cause : causes) {
213+
if ("ExitCode".equals(cause.getReason())) {
214+
try {
215+
return Integer.parseInt(cause.getMessage());
216+
} catch (NumberFormatException nfe) {
217+
log.error("Error parsing exit code from status channel response", nfe);
218+
}
219+
}
220+
}
221+
}
222+
}
223+
}
224+
} catch (Throwable t) {
225+
log.error("Error parsing exit code from status channel response", t);
226+
}
227+
228+
// Unable to parse the exit code from the content
229+
return -1;
230+
}
231+
232+
private class ExecProcess extends Process {
233+
private final WebSocketStreamHandler streamHandler;
234+
private int statusCode = -1;
235+
private boolean isAlive = true;
236+
private final Map<Integer, InputStream> input = new HashMap<>();
237+
238+
public ExecProcess() throws IOException {
239+
this.streamHandler =
240+
new WebSocketStreamHandler() {
241+
@Override
242+
protected void handleMessage(int stream, InputStream inStream) {
243+
if (stream == 3) {
244+
int exitCode = parseExitCode(apiClient, inStream);
245+
if (exitCode >= 0) {
246+
// notify of process completion
247+
synchronized (ExecProcess.this) {
248+
statusCode = exitCode;
249+
isAlive = false;
250+
ExecProcess.this.notifyAll();
251+
}
252+
}
253+
} else super.handleMessage(stream, inStream);
254+
}
255+
256+
@Override
257+
public void close() {
258+
// notify of process completion
259+
synchronized (ExecProcess.this) {
260+
if (isAlive) {
261+
isAlive = false;
262+
ExecProcess.this.notifyAll();
263+
}
264+
}
183265

184-
public ExecProcess(WebSocketStreamHandler handler) throws IOException {
185-
this.streamHandler = handler;
186-
this.statusCode = -1;
266+
super.close();
267+
}
268+
};
269+
}
270+
271+
private WebSocketStreamHandler getHandler() {
272+
return streamHandler;
187273
}
188274

189275
@Override
@@ -193,28 +279,58 @@ public OutputStream getOutputStream() {
193279

194280
@Override
195281
public InputStream getInputStream() {
196-
return streamHandler.getInputStream(1);
282+
return getInputStream(1);
197283
}
198284

199285
@Override
200286
public InputStream getErrorStream() {
201-
return streamHandler.getInputStream(2);
287+
return getInputStream(2);
288+
}
289+
290+
private synchronized InputStream getInputStream(int stream) {
291+
if (!input.containsKey(stream)) {
292+
input.put(stream, streamHandler.getInputStream(stream));
293+
}
294+
return input.get(stream);
202295
}
203296

204297
@Override
205298
public int waitFor() throws InterruptedException {
206299
synchronized (this) {
207300
this.wait();
301+
return statusCode;
302+
}
303+
}
304+
305+
@Override
306+
public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
307+
synchronized (this) {
308+
this.wait(TimeUnit.MILLISECONDS.convert(timeout, unit));
309+
return !isAlive();
208310
}
209-
return statusCode;
210311
}
211312

212-
public int exitValue() {
313+
@Override
314+
public synchronized int exitValue() {
315+
if (isAlive) throw new IllegalThreadStateException();
213316
return statusCode;
214317
}
215318

319+
@Override
320+
public synchronized boolean isAlive() {
321+
return isAlive;
322+
}
323+
324+
@Override
216325
public void destroy() {
217326
streamHandler.close();
327+
for (InputStream in : input.values()) {
328+
try {
329+
in.close();
330+
} catch (IOException ex) {
331+
log.error("Error on close", ex);
332+
}
333+
}
218334
}
219335
}
220336
}

0 commit comments

Comments
 (0)