embulk-input-remote 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
-
}
|