embulk-input-remote 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/build.gradle +21 -10
- data/classpath/embulk-input-remote-0.3.0.jar +0 -0
- data/classpath/kotlin-runtime-1.0.6.jar +0 -0
- data/classpath/kotlin-stdlib-1.0.6.jar +0 -0
- data/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/gradle/wrapper/gradle-wrapper.properties +2 -2
- data/gradlew +15 -7
- data/gradlew.bat +0 -6
- data/settings.gradle +3 -0
- data/src/main/kotlin/org/embulk/input/RemoteFileInputPlugin.kt +216 -0
- data/src/main/kotlin/org/embulk/input/remote/SSHClient.kt +74 -0
- data/src/test/kotlin/org/embulk/input/TestRemoteFileInputPlugin.kt +201 -0
- data/src/test/resources/script/hosts.sh +2 -0
- metadata +10 -11
- data/classpath/embulk-input-remote-0.2.0.jar +0 -0
- data/example/csv/sample_01.csv.gz +0 -0
- data/example/example.yml.liquid +0 -29
- data/src/main/java/org/embulk/input/RemoteFileInputPlugin.java +0 -314
- data/src/main/java/org/embulk/input/remote/SSHClient.java +0 -116
- data/src/test/java/org/embulk/input/TestRemoteFileInputPlugin.java +0 -255
- data/src/test/java/org/embulk/test/MemoryOutputPlugin.java +0 -143
- data/src/test/java/org/embulk/test/MyEmbulkTests.java +0 -23
- data/src/test/java/org/embulk/test/MyTestingEmbulk.java +0 -92
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: embulk-input-remote
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shinichi Ishimura
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,29 +52,28 @@ files:
|
|
52
52
|
- README.md
|
53
53
|
- build.gradle
|
54
54
|
- docker-compose.yml
|
55
|
-
- example/csv/sample_01.csv.gz
|
56
|
-
- example/example.yml.liquid
|
57
55
|
- gradle/wrapper/gradle-wrapper.jar
|
58
56
|
- gradle/wrapper/gradle-wrapper.properties
|
59
57
|
- gradlew
|
60
58
|
- gradlew.bat
|
61
59
|
- lib/embulk/input/remote.rb
|
62
|
-
-
|
63
|
-
- src/main/
|
64
|
-
- src/
|
65
|
-
- src/test/
|
66
|
-
- src/test/java/org/embulk/test/MyEmbulkTests.java
|
67
|
-
- src/test/java/org/embulk/test/MyTestingEmbulk.java
|
60
|
+
- settings.gradle
|
61
|
+
- src/main/kotlin/org/embulk/input/RemoteFileInputPlugin.kt
|
62
|
+
- src/main/kotlin/org/embulk/input/remote/SSHClient.kt
|
63
|
+
- src/test/kotlin/org/embulk/input/TestRemoteFileInputPlugin.kt
|
68
64
|
- src/test/resources/input/host1/test.csv
|
69
65
|
- src/test/resources/input/host1/test_command.csv
|
70
66
|
- src/test/resources/input/host2/test.csv
|
71
67
|
- src/test/resources/input/host2/test_command.csv
|
68
|
+
- src/test/resources/script/hosts.sh
|
72
69
|
- src/test/resources/yaml/base.yml
|
73
70
|
- classpath/bcpkix-jdk15on-1.51.jar
|
74
71
|
- classpath/bcprov-jdk15on-1.51.jar
|
75
72
|
- classpath/eddsa-0.1.0.jar
|
76
|
-
- classpath/embulk-input-remote-0.
|
73
|
+
- classpath/embulk-input-remote-0.3.0.jar
|
77
74
|
- classpath/jzlib-1.1.3.jar
|
75
|
+
- classpath/kotlin-runtime-1.0.6.jar
|
76
|
+
- classpath/kotlin-stdlib-1.0.6.jar
|
78
77
|
- classpath/sshj-0.19.1.jar
|
79
78
|
homepage: https://github.com/kamatama41/embulk-input-remote
|
80
79
|
licenses:
|
Binary file
|
Binary file
|
data/example/example.yml.liquid
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
in:
|
2
|
-
type: remote
|
3
|
-
hosts:
|
4
|
-
- localhost
|
5
|
-
path: {{ env.PROJECT_ROOT }}/example/csv
|
6
|
-
ignore_not_found_hosts: true
|
7
|
-
auth:
|
8
|
-
user: {{ env.USER }}
|
9
|
-
type: password
|
10
|
-
password: {{ env.PASSWORD }}
|
11
|
-
decoders:
|
12
|
-
- {type: gzip}
|
13
|
-
parser:
|
14
|
-
charset: UTF-8
|
15
|
-
newline: CRLF
|
16
|
-
type: csv
|
17
|
-
delimiter: ','
|
18
|
-
quote: '"'
|
19
|
-
trim_if_not_quoted: false
|
20
|
-
skip_header_lines: 1
|
21
|
-
allow_extra_columns: false
|
22
|
-
allow_optional_columns: false
|
23
|
-
columns:
|
24
|
-
- {name: id, type: long}
|
25
|
-
- {name: account, type: long}
|
26
|
-
- {name: time, type: timestamp, format: '%Y-%m-%d %H:%M:%S'}
|
27
|
-
- {name: purchase, type: timestamp, format: '%Y%m%d'}
|
28
|
-
- {name: comment, type: string}
|
29
|
-
out: {type: stdout}
|
@@ -1,314 +0,0 @@
|
|
1
|
-
package org.embulk.input;
|
2
|
-
|
3
|
-
import java.io.BufferedReader;
|
4
|
-
import java.io.ByteArrayInputStream;
|
5
|
-
import java.io.ByteArrayOutputStream;
|
6
|
-
import java.io.IOException;
|
7
|
-
import java.io.InputStream;
|
8
|
-
import java.io.InputStreamReader;
|
9
|
-
import java.util.ArrayList;
|
10
|
-
import java.util.Arrays;
|
11
|
-
import java.util.List;
|
12
|
-
import java.util.Objects;
|
13
|
-
|
14
|
-
import com.fasterxml.jackson.annotation.JsonCreator;
|
15
|
-
import com.fasterxml.jackson.annotation.JsonProperty;
|
16
|
-
import com.google.common.base.Optional;
|
17
|
-
import com.google.common.collect.ImmutableList;
|
18
|
-
import org.embulk.config.Config;
|
19
|
-
import org.embulk.config.ConfigDefault;
|
20
|
-
import org.embulk.config.ConfigDiff;
|
21
|
-
import org.embulk.config.ConfigInject;
|
22
|
-
import org.embulk.config.ConfigSource;
|
23
|
-
import org.embulk.config.Task;
|
24
|
-
import org.embulk.config.TaskReport;
|
25
|
-
import org.embulk.config.TaskSource;
|
26
|
-
import org.embulk.input.remote.SSHClient;
|
27
|
-
import org.embulk.spi.BufferAllocator;
|
28
|
-
import org.embulk.spi.Exec;
|
29
|
-
import org.embulk.spi.FileInputPlugin;
|
30
|
-
import org.embulk.spi.TransactionalFileInput;
|
31
|
-
import org.embulk.spi.util.InputStreamTransactionalFileInput;
|
32
|
-
import org.slf4j.Logger;
|
33
|
-
|
34
|
-
public class RemoteFileInputPlugin
|
35
|
-
implements FileInputPlugin {
|
36
|
-
public interface PluginTask
|
37
|
-
extends Task {
|
38
|
-
@Config("hosts")
|
39
|
-
@ConfigDefault("[]")
|
40
|
-
List<String> getHosts();
|
41
|
-
|
42
|
-
@Config("hosts_command")
|
43
|
-
@ConfigDefault("null")
|
44
|
-
Optional<String> getHostsCommand();
|
45
|
-
|
46
|
-
@Config("hosts_separator")
|
47
|
-
@ConfigDefault("\" \"")
|
48
|
-
String getHostsSeparator();
|
49
|
-
|
50
|
-
@Config("default_port")
|
51
|
-
@ConfigDefault("22")
|
52
|
-
int getDefaultPort();
|
53
|
-
|
54
|
-
@Config("path")
|
55
|
-
@ConfigDefault("\"\"")
|
56
|
-
String getPath();
|
57
|
-
|
58
|
-
@Config("path_command")
|
59
|
-
@ConfigDefault("null")
|
60
|
-
Optional<String> getPathCommand();
|
61
|
-
|
62
|
-
@Config("auth")
|
63
|
-
AuthConfig getAuthConfig();
|
64
|
-
|
65
|
-
@Config("ignore_not_found_hosts")
|
66
|
-
@ConfigDefault("false")
|
67
|
-
boolean getIgnoreNotFoundHosts();
|
68
|
-
|
69
|
-
@Config("done_targets")
|
70
|
-
@ConfigDefault("[]")
|
71
|
-
List<Target> getDoneTargets();
|
72
|
-
|
73
|
-
void setDoneTargets(List<Target> lastTarget);
|
74
|
-
|
75
|
-
List<Target> getTargets();
|
76
|
-
|
77
|
-
void setTargets(List<Target> targets);
|
78
|
-
|
79
|
-
@ConfigInject
|
80
|
-
BufferAllocator getBufferAllocator();
|
81
|
-
}
|
82
|
-
|
83
|
-
public interface AuthConfig extends Task {
|
84
|
-
@Config("type")
|
85
|
-
@ConfigDefault("\"public_key\"")
|
86
|
-
String getType();
|
87
|
-
|
88
|
-
@Config("user")
|
89
|
-
@ConfigDefault("null")
|
90
|
-
Optional<String> getUser();
|
91
|
-
|
92
|
-
@Config("key_path")
|
93
|
-
@ConfigDefault("null")
|
94
|
-
Optional<String> getKeyPath();
|
95
|
-
|
96
|
-
@Config("password")
|
97
|
-
@ConfigDefault("null")
|
98
|
-
Optional<String> getPassword();
|
99
|
-
|
100
|
-
@Config("skip_host_key_verification")
|
101
|
-
@ConfigDefault("false")
|
102
|
-
boolean getSkipHostKeyVerification();
|
103
|
-
}
|
104
|
-
|
105
|
-
private final Logger log = Exec.getLogger(getClass());
|
106
|
-
|
107
|
-
@Override
|
108
|
-
public ConfigDiff transaction(ConfigSource config, FileInputPlugin.Control control) {
|
109
|
-
PluginTask task = config.loadConfig(PluginTask.class);
|
110
|
-
List<Target> targets = listTargets(task);
|
111
|
-
log.info("Loading targets {}", targets);
|
112
|
-
task.setTargets(targets);
|
113
|
-
|
114
|
-
// number of processors is same with number of targets
|
115
|
-
int taskCount = targets.size();
|
116
|
-
return resume(task.dump(), taskCount, control);
|
117
|
-
}
|
118
|
-
|
119
|
-
private List<Target> listTargets(PluginTask task) {
|
120
|
-
final List<String> hosts = listHosts(task);
|
121
|
-
final String path = getPath(task);
|
122
|
-
|
123
|
-
final ImmutableList.Builder<Target> builder = ImmutableList.builder();
|
124
|
-
List<Target> doneTargets = task.getDoneTargets();
|
125
|
-
for (String host : hosts) {
|
126
|
-
String[] split = host.split(":");
|
127
|
-
final String targetHost = split[0];
|
128
|
-
int targetPort = task.getDefaultPort();
|
129
|
-
if (split.length > 1) {
|
130
|
-
targetPort = Integer.valueOf(split[1]);
|
131
|
-
}
|
132
|
-
Target target = new Target(targetHost, targetPort, path);
|
133
|
-
|
134
|
-
if (!doneTargets.contains(target)) {
|
135
|
-
if (task.getIgnoreNotFoundHosts()) {
|
136
|
-
try {
|
137
|
-
final boolean exists = exists(target, task);
|
138
|
-
if (!exists) {
|
139
|
-
continue;
|
140
|
-
}
|
141
|
-
} catch (IOException e) {
|
142
|
-
log.warn("failed to check the file exists. " + target.toString(), e);
|
143
|
-
continue;
|
144
|
-
}
|
145
|
-
}
|
146
|
-
builder.add(target);
|
147
|
-
}
|
148
|
-
}
|
149
|
-
return builder.build();
|
150
|
-
}
|
151
|
-
|
152
|
-
private List<String> listHosts(PluginTask task) {
|
153
|
-
final String hostsCommand = task.getHostsCommand().orNull();
|
154
|
-
if (hostsCommand != null) {
|
155
|
-
final String stdout = execCommand(hostsCommand).trim();
|
156
|
-
return Arrays.asList(stdout.split(task.getHostsSeparator()));
|
157
|
-
} else {
|
158
|
-
return task.getHosts();
|
159
|
-
}
|
160
|
-
}
|
161
|
-
|
162
|
-
private String getPath(PluginTask task) {
|
163
|
-
final String pathCommand = task.getPathCommand().orNull();
|
164
|
-
if (pathCommand != null) {
|
165
|
-
return execCommand(pathCommand).trim();
|
166
|
-
} else {
|
167
|
-
return task.getPath();
|
168
|
-
}
|
169
|
-
}
|
170
|
-
|
171
|
-
private String execCommand(String command) {
|
172
|
-
ProcessBuilder pb = new ProcessBuilder("sh", "-c", command); // TODO: windows
|
173
|
-
log.info("Running command {}", command);
|
174
|
-
try {
|
175
|
-
final Process process = pb.start();
|
176
|
-
try (InputStream stream = process.getInputStream();
|
177
|
-
BufferedReader brStdout = new BufferedReader(new InputStreamReader(stream))
|
178
|
-
) {
|
179
|
-
String line;
|
180
|
-
StringBuilder stdout = new StringBuilder();
|
181
|
-
while ((line = brStdout.readLine()) != null) {
|
182
|
-
stdout.append(line);
|
183
|
-
}
|
184
|
-
|
185
|
-
final int code = process.waitFor();
|
186
|
-
if (code != 0) {
|
187
|
-
throw new IOException(String.format(
|
188
|
-
"Command finished with non-zero exit code. Exit code is %d.", code));
|
189
|
-
}
|
190
|
-
|
191
|
-
return stdout.toString();
|
192
|
-
}
|
193
|
-
} catch (IOException | InterruptedException e) {
|
194
|
-
throw new RuntimeException(e);
|
195
|
-
}
|
196
|
-
}
|
197
|
-
|
198
|
-
@Override
|
199
|
-
public ConfigDiff resume(TaskSource taskSource,
|
200
|
-
int taskCount,
|
201
|
-
FileInputPlugin.Control control) {
|
202
|
-
PluginTask task = taskSource.loadTask(PluginTask.class);
|
203
|
-
|
204
|
-
control.run(taskSource, taskCount);
|
205
|
-
|
206
|
-
List<Target> targets = new ArrayList<>(task.getTargets());
|
207
|
-
|
208
|
-
return Exec.newConfigDiff().set("done_targets", targets);
|
209
|
-
}
|
210
|
-
|
211
|
-
@Override
|
212
|
-
public void cleanup(TaskSource taskSource,
|
213
|
-
int taskCount,
|
214
|
-
List<TaskReport> successTaskReports) {
|
215
|
-
}
|
216
|
-
|
217
|
-
@Override
|
218
|
-
public TransactionalFileInput open(TaskSource taskSource, int taskIndex) {
|
219
|
-
final PluginTask task = taskSource.loadTask(PluginTask.class);
|
220
|
-
final Target target = task.getTargets().get(taskIndex);
|
221
|
-
|
222
|
-
return new InputStreamTransactionalFileInput(
|
223
|
-
task.getBufferAllocator(),
|
224
|
-
new InputStreamTransactionalFileInput.Opener() {
|
225
|
-
@Override
|
226
|
-
public InputStream open() throws IOException {
|
227
|
-
return download(target, task);
|
228
|
-
}
|
229
|
-
}
|
230
|
-
) {
|
231
|
-
@Override
|
232
|
-
public void abort() {
|
233
|
-
|
234
|
-
}
|
235
|
-
|
236
|
-
@Override
|
237
|
-
public TaskReport commit() {
|
238
|
-
return Exec.newTaskReport();
|
239
|
-
}
|
240
|
-
};
|
241
|
-
}
|
242
|
-
|
243
|
-
private boolean exists(Target target, PluginTask task) throws IOException {
|
244
|
-
try (SSHClient client = SSHClient.connect(target.getHost(), target.getPort(), task.getAuthConfig())) {
|
245
|
-
final String checkCmd = "ls " + target.getPath(); // TODO: windows
|
246
|
-
final int timeout = 5/* second */;
|
247
|
-
final SSHClient.CommandResult commandResult = client.execCommand(checkCmd, timeout);
|
248
|
-
|
249
|
-
if(commandResult.getStatus() != 0) {
|
250
|
-
log.warn("Remote file not found. {}", target.toString());
|
251
|
-
return false;
|
252
|
-
} else {
|
253
|
-
return true;
|
254
|
-
}
|
255
|
-
}
|
256
|
-
}
|
257
|
-
|
258
|
-
private InputStream download(Target target, PluginTask task) throws IOException {
|
259
|
-
try (SSHClient client = SSHClient.connect(target.getHost(), target.getPort(), task.getAuthConfig())) {
|
260
|
-
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
261
|
-
client.scpDownload(target.getPath(), stream);
|
262
|
-
return new ByteArrayInputStream(stream.toByteArray());
|
263
|
-
}
|
264
|
-
}
|
265
|
-
|
266
|
-
public static class Target {
|
267
|
-
private final String host;
|
268
|
-
private final int port;
|
269
|
-
private final String path;
|
270
|
-
|
271
|
-
@JsonCreator
|
272
|
-
public Target(
|
273
|
-
@JsonProperty("host") String host,
|
274
|
-
@JsonProperty("port") int port,
|
275
|
-
@JsonProperty("path") String path
|
276
|
-
) {
|
277
|
-
this.host = host;
|
278
|
-
this.port = port;
|
279
|
-
this.path = path;
|
280
|
-
}
|
281
|
-
|
282
|
-
public String getHost() {
|
283
|
-
return host;
|
284
|
-
}
|
285
|
-
|
286
|
-
public int getPort() {
|
287
|
-
return port;
|
288
|
-
}
|
289
|
-
|
290
|
-
public String getPath() {
|
291
|
-
return path;
|
292
|
-
}
|
293
|
-
|
294
|
-
@Override
|
295
|
-
public boolean equals(Object o) {
|
296
|
-
if (this == o) return true;
|
297
|
-
if (o == null || getClass() != o.getClass()) return false;
|
298
|
-
Target target = (Target) o;
|
299
|
-
return port == target.port &&
|
300
|
-
Objects.equals(host, target.host) &&
|
301
|
-
Objects.equals(path, target.path);
|
302
|
-
}
|
303
|
-
|
304
|
-
@Override
|
305
|
-
public int hashCode() {
|
306
|
-
return Objects.hash(host, port, path);
|
307
|
-
}
|
308
|
-
|
309
|
-
@Override
|
310
|
-
public String toString() {
|
311
|
-
return host + ":" + port + ":" + path;
|
312
|
-
}
|
313
|
-
}
|
314
|
-
}
|
@@ -1,116 +0,0 @@
|
|
1
|
-
package org.embulk.input.remote;
|
2
|
-
|
3
|
-
import net.schmizz.sshj.DefaultConfig;
|
4
|
-
import net.schmizz.sshj.connection.channel.direct.Session;
|
5
|
-
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
|
6
|
-
import net.schmizz.sshj.xfer.InMemoryDestFile;
|
7
|
-
import net.schmizz.sshj.xfer.LocalDestFile;
|
8
|
-
import org.embulk.input.RemoteFileInputPlugin;
|
9
|
-
|
10
|
-
import java.io.Closeable;
|
11
|
-
import java.io.IOException;
|
12
|
-
import java.io.InputStream;
|
13
|
-
import java.io.OutputStream;
|
14
|
-
import java.util.concurrent.TimeUnit;
|
15
|
-
|
16
|
-
public class SSHClient implements Closeable {
|
17
|
-
|
18
|
-
private final net.schmizz.sshj.SSHClient client;
|
19
|
-
|
20
|
-
public static SSHClient connect(
|
21
|
-
String host, int port, RemoteFileInputPlugin.AuthConfig authConfig
|
22
|
-
) throws IOException {
|
23
|
-
|
24
|
-
SSHClient client = new SSHClient(new net.schmizz.sshj.SSHClient(new DefaultConfig()));
|
25
|
-
client.connectToHost(host, port, authConfig);
|
26
|
-
return client;
|
27
|
-
}
|
28
|
-
|
29
|
-
private SSHClient(net.schmizz.sshj.SSHClient client) {
|
30
|
-
this.client = client;
|
31
|
-
}
|
32
|
-
|
33
|
-
private void connectToHost(String host, int port, RemoteFileInputPlugin.AuthConfig authConfig) throws IOException {
|
34
|
-
if (authConfig.getSkipHostKeyVerification()) {
|
35
|
-
client.addHostKeyVerifier(new PromiscuousVerifier());
|
36
|
-
}
|
37
|
-
client.loadKnownHosts();
|
38
|
-
client.connect(host, port);
|
39
|
-
|
40
|
-
final String type = authConfig.getType();
|
41
|
-
final String user = authConfig.getUser().or(System.getProperty("user.name"));
|
42
|
-
|
43
|
-
if ("password".equals(type)) {
|
44
|
-
if (authConfig.getPassword().isPresent()) {
|
45
|
-
client.authPassword(user, authConfig.getPassword().get());
|
46
|
-
} else {
|
47
|
-
throw new IllegalStateException("Password is not set.");
|
48
|
-
}
|
49
|
-
} else if ("public_key".equals(type)) {
|
50
|
-
if (authConfig.getKeyPath().isPresent()) {
|
51
|
-
client.authPublickey(user, authConfig.getKeyPath().get());
|
52
|
-
} else {
|
53
|
-
client.authPublickey(user);
|
54
|
-
}
|
55
|
-
} else {
|
56
|
-
throw new UnsupportedOperationException("Unsupported auth type : " + type);
|
57
|
-
}
|
58
|
-
}
|
59
|
-
|
60
|
-
public CommandResult execCommand(String command, int timeoutSecond) throws IOException {
|
61
|
-
try (final Session session = client.startSession()) {
|
62
|
-
final Session.Command cmd = session.exec(command);
|
63
|
-
cmd.join(timeoutSecond, TimeUnit.SECONDS);
|
64
|
-
return new CommandResult(cmd.getExitStatus(), cmd.getInputStream());
|
65
|
-
}
|
66
|
-
}
|
67
|
-
|
68
|
-
public void scpDownload(String path, OutputStream stream) throws IOException {
|
69
|
-
client.useCompression();
|
70
|
-
client.newSCPFileTransfer().download(path, new InMemoryDestFileImpl(stream));
|
71
|
-
}
|
72
|
-
|
73
|
-
private static class InMemoryDestFileImpl extends InMemoryDestFile {
|
74
|
-
|
75
|
-
private OutputStream outputStream;
|
76
|
-
|
77
|
-
public InMemoryDestFileImpl(OutputStream outputStream) {
|
78
|
-
this.outputStream = outputStream;
|
79
|
-
}
|
80
|
-
|
81
|
-
@Override
|
82
|
-
public OutputStream getOutputStream() throws IOException {
|
83
|
-
return outputStream;
|
84
|
-
}
|
85
|
-
|
86
|
-
@Override
|
87
|
-
public LocalDestFile getTargetDirectory(String dirname) throws IOException {
|
88
|
-
return this;
|
89
|
-
}
|
90
|
-
}
|
91
|
-
|
92
|
-
@Override
|
93
|
-
public void close() throws IOException {
|
94
|
-
if (client != null) {
|
95
|
-
client.close();
|
96
|
-
}
|
97
|
-
}
|
98
|
-
|
99
|
-
public static class CommandResult {
|
100
|
-
int status;
|
101
|
-
InputStream stdout;
|
102
|
-
|
103
|
-
private CommandResult(int status, InputStream stdout) {
|
104
|
-
this.status = status;
|
105
|
-
this.stdout = stdout;
|
106
|
-
}
|
107
|
-
|
108
|
-
public int getStatus() {
|
109
|
-
return status;
|
110
|
-
}
|
111
|
-
|
112
|
-
public InputStream getStdout() {
|
113
|
-
return stdout;
|
114
|
-
}
|
115
|
-
}
|
116
|
-
}
|