Skip to content

Commit c0163a1

Browse files
Add killport (#2)
* add kill-port project, start investigating reqmnts * group terminal and system software together * add os detection * implement killport, add unknown os, tests, jacoco * add graalvm gradle plugin * working nativeImage on linux * move shadow plugin to conventions
1 parent 8aa2e71 commit c0163a1

File tree

18 files changed

+355
-1
lines changed

18 files changed

+355
-1
lines changed

buildSrc/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@ plugins {
44

55
repositories {
66
gradlePluginPortal()
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
// https://stackoverflow.com/a/66897652
12+
implementation 'org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.11'
13+
implementation 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2'
714
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
plugins {
22
id 'info.ankin.projects.conventions'
33
id 'application'
4+
id 'org.graalvm.buildtools.native'
5+
id 'com.github.johnrengelman.shadow'
46
}

buildSrc/src/main/groovy/info.ankin.projects.conventions.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
id 'java'
3+
id 'jacoco'
34
}
45

56
group = 'info.ankin.projects'

cli/killport/build.gradle

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
plugins {
2+
id 'info.ankin.projects.app-conventions'
3+
}
4+
5+
version = '0.0.1'
6+
7+
dependencies {
8+
implementation project(':system:infer-platform')
9+
implementation 'commons-io:commons-io:2.11.0'
10+
implementation 'org.apache.commons:commons-lang3:3.12.0'
11+
}
12+
13+
application {
14+
mainClass.set 'info.ankin.projects.cli.killport.KillPort'
15+
}
16+
17+
// https://graalvm.github.io/native-build-tools/0.9.11/gradle-plugin.html#_configuration_options
18+
graalvmNative {
19+
binaries {
20+
main {
21+
// Main options
22+
imageName.set 'killport'
23+
mainClass.set application.mainClass.get()
24+
25+
debug.set false
26+
verbose.set true
27+
fallback.set true
28+
// sharedLibrary.set false
29+
// systemProperties = System.getProperties();
30+
31+
// Adds a native image configuration file directory, containing files like reflection configuration
32+
// configurationFileDirectories.from(file('src/my-config'))
33+
34+
// // Passes '-H:Extra' to the native image builder options.
35+
// // This can be used to pass parameters which are not directly supported by this extension
36+
// buildArgs.add('-H:Extra')
37+
// buildArgs.add('-Ob') // faster development builds
38+
//
39+
// // Passes 'flag' directly to the JVM running the native image builder
40+
// jvmArgs.add('flag')
41+
42+
useFatJar.set true
43+
}
44+
}
45+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package info.ankin.projects.cli.killport;
2+
3+
import info.ankin.projects.infer_platform.Os;
4+
import info.ankin.projects.infer_platform.PlatformInferrer;
5+
import lombok.SneakyThrows;
6+
import org.apache.commons.io.IOUtils;
7+
import org.apache.commons.lang3.math.NumberUtils;
8+
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.*;
13+
import java.util.regex.Matcher;
14+
import java.util.regex.Pattern;
15+
import java.util.stream.Collectors;
16+
17+
public class KillPort {
18+
final List<Integer> ports;
19+
final Protocol method;
20+
final PlatformInferrer platformInferrer;
21+
22+
/**
23+
* Parses inputs
24+
* <p>
25+
* currently defaults to TCP instead of parsing <code>--protocol</code> argument.
26+
*
27+
* @param args inputs
28+
* @param inferrer helper to tell which platform we are running on
29+
*/
30+
public KillPort(String[] args, PlatformInferrer inferrer) {
31+
ports = Arrays.stream(args)
32+
.filter(NumberUtils::isCreatable)
33+
.map(NumberUtils::createInteger)
34+
.collect(Collectors.toCollection(ArrayList::new));
35+
method = Protocol.TCP;
36+
this.platformInferrer = inferrer;
37+
}
38+
39+
public static void main(String[] args) {
40+
// dependencies
41+
new KillPort(args, new PlatformInferrer()).run();
42+
}
43+
44+
public void run() {
45+
System.out.println("hello, world!");
46+
Os os = platformInferrer.os();
47+
System.out.println("we are running on " + os + "!");
48+
System.out.println("we are removing processes listening on ports: " + ports);
49+
run(os);
50+
}
51+
52+
@SneakyThrows
53+
private void run(Os os) {
54+
switch (os) {
55+
case WINDOWS:
56+
String output = execNetstatWindows();
57+
for (Integer port : ports) {
58+
Set<Integer> pidSet = parseWindowsOutput(output, method, port);
59+
if (pidSet.isEmpty()) continue;
60+
61+
List<String> pidList = pidSet.stream()
62+
.map(Object::toString)
63+
.collect(Collectors.toList());
64+
65+
StringJoiner joiner = new StringJoiner(" /PID ", "/PID ", "");
66+
pidList.forEach(joiner::add);
67+
String pidFlags = joiner.toString();
68+
exec("TaskKill /F " + pidFlags);
69+
}
70+
break;
71+
case LINUX:
72+
case DARWIN:
73+
String interfaceName = method.lsofInterfaceName();
74+
String nameFdMode = method.lsofOutputNameColumnFdMode();
75+
for (Integer port : ports) {
76+
exec("lsof -ni " + interfaceName + ":" + port + " | grep " + nameFdMode + " | awk '{print $2}' | xargs kill -9");
77+
}
78+
break;
79+
default:
80+
throw new UnsupportedOperationException("Not supported yet: " + os + " (" + System.getProperty("os.name") + ")");
81+
}
82+
}
83+
84+
Set<Integer> parseWindowsOutput(String output, Protocol method, Integer port) {
85+
// whitespace, followed by: non-ws:$port, anything, ws, PID
86+
Pattern pidFinder = Pattern.compile("^ *" + method + " *[^ ]*:" + port + ".* +(\\d+)", Pattern.MULTILINE);
87+
Matcher matcher = pidFinder.matcher(output);
88+
return matcher.results().map(e -> e.group(1)).filter(NumberUtils::isCreatable).map(NumberUtils::createInteger).collect(Collectors.toSet());
89+
}
90+
91+
String execNetstatWindows() throws IOException, InterruptedException {
92+
return exec("netstat -nao");
93+
}
94+
95+
String exec(String command) throws IOException, InterruptedException {
96+
ProcessBuilder processBuilder = new ProcessBuilder();
97+
String shell = platformInferrer.os() == Os.WINDOWS ? "cmd" : "sh";
98+
String flag = platformInferrer.os() == Os.WINDOWS ? "/C" : "-c";
99+
processBuilder.command(shell, flag, command);
100+
Process process = processBuilder.start();
101+
InputStream output = process.getInputStream();
102+
int exitCode = process.waitFor();
103+
System.out.println("process exited with code: " + exitCode);
104+
return IOUtils.toString(output, StandardCharsets.UTF_8);
105+
}
106+
107+
enum Protocol {
108+
TCP {
109+
@Override
110+
String lsofOutputNameColumnFdMode() {
111+
return "LISTEN";
112+
}
113+
},
114+
UDP,
115+
;
116+
117+
String lsofInterfaceName() {
118+
return name().toLowerCase();
119+
}
120+
121+
String lsofOutputNameColumnFdMode() {
122+
return name();
123+
}
124+
}
125+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package info.ankin.projects.cli.killport;
2+
3+
import info.ankin.projects.infer_platform.Os;
4+
import info.ankin.projects.infer_platform.PlatformInferrer;
5+
import lombok.SneakyThrows;
6+
import org.apache.commons.io.IOUtils;
7+
import org.apache.commons.lang3.tuple.Pair;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
11+
import java.nio.charset.StandardCharsets;
12+
import java.nio.file.Path;
13+
import java.util.ArrayDeque;
14+
import java.util.Deque;
15+
import java.util.List;
16+
import java.util.Objects;
17+
18+
import static org.hamcrest.MatcherAssert.assertThat;
19+
import static org.hamcrest.Matchers.*;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.when;
23+
24+
class KillPortTest {
25+
26+
private static final Path path = Path.of("/info/ankin/projects/cli/killport");
27+
28+
KillPort killPort;
29+
PlatformInferrer platformInferrer;
30+
Deque<String> execArgs = new ArrayDeque<>();
31+
Deque<String> execResults = new ArrayDeque<>();
32+
Deque<String> execNetstatWinResults = new ArrayDeque<>();
33+
34+
@BeforeEach
35+
void setUp() {
36+
platformInferrer = mock(PlatformInferrer.class);
37+
killPort = new KillPort(new String[]{}, platformInferrer) {
38+
final Deque<String> execResults = KillPortTest.this.execResults;
39+
final Deque<String> execNetstatWinResults = KillPortTest.this.execNetstatWinResults;
40+
41+
@Override
42+
String execNetstatWindows() {
43+
return execNetstatWinResults.pop();
44+
}
45+
46+
@Override
47+
protected String exec(String s) {
48+
execArgs.push(s);
49+
return execResults.isEmpty() ? null : execResults.pop();
50+
}
51+
};
52+
}
53+
54+
@SneakyThrows
55+
String getSampleInput() {
56+
return IOUtils.toString(Objects.requireNonNull(getClass().getResourceAsStream(path.resolve("example-netstat-windows-output.txt").toString())), StandardCharsets.UTF_8);
57+
}
58+
59+
@Test
60+
void test_parsingWindowsOutput() {
61+
String output = getSampleInput();
62+
63+
assertThat(killPort.parseWindowsOutput(output, KillPort.Protocol.TCP, 49155), contains(432));
64+
assertThat(killPort.parseWindowsOutput(output, KillPort.Protocol.TCP, 445), contains(4));
65+
assertThat(killPort.parseWindowsOutput(output, KillPort.Protocol.TCP, 446), empty());
66+
}
67+
68+
69+
@Test
70+
void test_win() {
71+
when(platformInferrer.os()).thenReturn(Os.WINDOWS);
72+
String output = getSampleInput();
73+
74+
List<Pair<Integer, Integer>> ports = List.of(Pair.of(49155, 432), Pair.of(445, 4), Pair.of(446, null));
75+
76+
for (Pair<Integer, Integer> pair : ports) {
77+
Integer port = pair.getLeft();
78+
killPort.ports.clear();
79+
killPort.ports.add(port);
80+
execNetstatWinResults.push(output);
81+
killPort.run();
82+
if (pair.getRight() == null) {
83+
assertThat(execArgs, empty());
84+
} else {
85+
String result = execArgs.pop();
86+
assertThat(result, is("TaskKill /F /PID " + pair.getRight()));
87+
}
88+
}
89+
}
90+
91+
@Test
92+
void test_linux() {
93+
when(platformInferrer.os()).thenReturn(Os.LINUX);
94+
killPort.ports.clear();
95+
killPort.ports.add(8080);
96+
killPort.run();
97+
String args = execArgs.pop();
98+
assertThat(args, is("lsof -ni tcp:8080 | grep LISTEN | awk '{print $2}' | xargs kill -9"));
99+
}
100+
101+
@Test
102+
void test_unknown() {
103+
when(platformInferrer.os()).thenReturn(Os.UNKNOWN);
104+
UnsupportedOperationException uoe =
105+
assertThrows(UnsupportedOperationException.class, killPort::run);
106+
String actual = System.getProperty("os.name");
107+
assertThat(uoe.getMessage(), is("Not supported yet: UNKNOWN (" + actual + ")"));
108+
109+
}
110+
111+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
C:\>netstat -ano
2+
3+
Active Connections
4+
5+
Proto Local Address Foreign Address State PID
6+
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 680
7+
TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4
8+
TCP 0.0.0.0:3389 0.0.0.0:0 LISTENING 1128
9+
TCP 0.0.0.0:49152 0.0.0.0:0 LISTENING 348
10+
TCP 0.0.0.0:49153 0.0.0.0:0 LISTENING 772
11+
TCP 0.0.0.0:49154 0.0.0.0:0 LISTENING 896
12+
TCP 0.0.0.0:49155 0.0.0.0:0 LISTENING 432
13+
TCP 0.0.0.0:49156 0.0.0.0:0 LISTENING 448
14+
TCP 10.0.2.15:139 0.0.0.0:0 LISTENING 4
15+
TCP [::]:135 [::]:0 LISTENING 680
16+
TCP [::]:445 [::]:0 LISTENING 4
17+
TCP [::]:3389 [::]:0 LISTENING 1128
18+
TCP [::]:49152 [::]:0 LISTENING 348
19+
TCP [::]:49153 [::]:0 LISTENING 772
20+
TCP [::]:49154 [::]:0 LISTENING 896
21+
TCP [::]:49155 [::]:0 LISTENING 432
22+
TCP [::]:49156 [::]:0 LISTENING 448
23+
TCP 0.0.0.0:5355 *:* LISTENING 1128

settings.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
rootProject.name = 'java-projects'
2-
include 'terminal:supports-hyperlinks'
2+
include 'cli:killport'
3+
include 'system:infer-platform'
4+
include 'system:terminal:supports-hyperlinks'

system/infer-platform/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
plugins {
2+
id 'info.ankin.projects.library-conventions'
3+
}
4+
5+
version = '0.0.1'
6+
7+
dependencies {
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package info.ankin.projects.infer_platform;
2+
3+
public enum Arch {
4+
x32,
5+
x64,
6+
m1,
7+
}

0 commit comments

Comments
 (0)