Skip to content

Commit 2d30e3c

Browse files
committed
PortInUseFailure provides PID info for port conflicts
The PortInUseFailureAnalyzer will perform a best effort attempt to detect the PID and process information of the port that bound to the port, and report it in the failure message. Signed-off-by: ygraber <[email protected]>
1 parent 5ae53e1 commit 2d30e3c

File tree

2 files changed

+222
-5
lines changed

2 files changed

+222
-5
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/PortInUseFailureAnalyzer.java

Lines changed: 151 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,170 @@
1616

1717
package org.springframework.boot.diagnostics.analyzer;
1818

19+
import java.io.BufferedReader;
20+
import java.io.IOException;
21+
import java.io.InputStreamReader;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.concurrent.TimeUnit;
25+
26+
import org.apache.commons.logging.Log;
27+
import org.apache.commons.logging.LogFactory;
28+
1929
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
2030
import org.springframework.boot.diagnostics.FailureAnalysis;
2131
import org.springframework.boot.web.server.PortInUseException;
2232

2333
/**
2434
* A {@code FailureAnalyzer} that performs analysis of failures caused by a
25-
* {@code PortInUseException}.
35+
* {@code PortInUseException}. <br/>
36+
* The analyzer attempts to find the process that is using the port and provides
37+
* information about it in the failure analysis.
2638
*
2739
* @author Andy Wilkinson
40+
* @author Yonatan Graber
2841
*/
2942
class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {
3043

44+
private static final Log logger = LogFactory.getLog(PortInUseFailureAnalyzer.class);
45+
3146
@Override
3247
protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
33-
return new FailureAnalysis("Web server failed to start. Port " + cause.getPort() + " was already in use.",
34-
"Identify and stop the process that's listening on port " + cause.getPort() + " or configure this "
35-
+ "application to listen on another port.",
36-
cause);
48+
ProcessInfo processInfo = findProcessUsingPort(cause.getPort());
49+
50+
String description = buildDescription(cause.getPort(), processInfo);
51+
String action = buildAction(cause.getPort(), processInfo);
52+
return new FailureAnalysis(description, action, cause);
53+
}
54+
55+
private String buildDescription(int port, ProcessInfo processInfo) {
56+
StringBuilder message = new StringBuilder();
57+
message.append("Web server failed to start. Port ").append(port).append(" was already in use");
58+
if (processInfo != null) {
59+
message.append(" by ").append(processInfo.command).append(" (PID: ").append(processInfo.pid).append(")");
60+
}
61+
message.append(".");
62+
return message.toString();
63+
}
64+
65+
private String buildAction(int port, ProcessInfo processInfo) {
66+
StringBuilder message = new StringBuilder();
67+
if (processInfo != null) {
68+
message.append("Stop the process ")
69+
.append(processInfo.command)
70+
.append(" (PID: ")
71+
.append(processInfo.pid)
72+
.append(")");
73+
}
74+
else {
75+
message.append("Identify and stop the process");
76+
}
77+
message.append(" that's listening on port ")
78+
.append(port)
79+
.append(" or configure this application to listen on another port.");
80+
return message.toString();
81+
}
82+
83+
/**
84+
* Find the process using the given port. Will invoke OS-specific commands to
85+
* determine the process ID and command name.
86+
* @param port the port to check
87+
* @return the process information or {@code null} if the process cannot be found for
88+
* any reason
89+
*/
90+
private ProcessInfo findProcessUsingPort(int port) {
91+
String os = System.getProperty("os.name").toLowerCase();
92+
try {
93+
if (os.contains("win")) {
94+
return findProcessOnWindows(port);
95+
}
96+
else if (os.contains("mac") || os.contains("nix") || os.contains("nux")) {
97+
return findProcessOnUnix(port);
98+
}
99+
else {
100+
logger.debug("Could not find process using port " + port + " in OS " + os);
101+
}
102+
}
103+
catch (Exception ex) {
104+
logger.warn("Unable to find process using port " + port, ex);
105+
}
106+
return null;
107+
}
108+
109+
private ProcessInfo findProcessOnWindows(int port) throws Exception {
110+
Process process = new ProcessBuilder("cmd.exe", "/c", "netstat -ano | findstr :" + port).start();
111+
waitForProcess(process);
112+
List<String> lines = readOutput(process);
113+
for (String line : lines) {
114+
line = line.trim();
115+
if (line.contains("LISTENING") || line.contains("ESTABLISHED")) {
116+
String[] parts = line.split("\\s+");
117+
if (parts.length >= 5) {
118+
String pid = parts[4];
119+
String command = getWindowsCommandByPid(pid);
120+
return new ProcessInfo(pid, command);
121+
}
122+
}
123+
}
124+
return null;
125+
}
126+
127+
private String getWindowsCommandByPid(String pid) throws Exception {
128+
Process process = new ProcessBuilder("cmd.exe", "/c", "tasklist /FI \"PID eq " + pid + "\"").start();
129+
waitForProcess(process);
130+
List<String> lines = readOutput(process);
131+
for (String line : lines) {
132+
if (line.startsWith("Image Name")) {
133+
continue;
134+
}
135+
if (line.toLowerCase().contains(pid)) {
136+
return line.split("\\s+")[0];
137+
}
138+
}
139+
return null;
140+
}
141+
142+
private ProcessInfo findProcessOnUnix(int port) throws IOException {
143+
Process process = new ProcessBuilder("lsof", "-nP", "-i", ":" + port).start();
144+
waitForProcess(process);
145+
List<String> lines = readOutput(process);
146+
for (String line : lines) {
147+
if (line.startsWith("COMMAND")) {
148+
continue; // header
149+
}
150+
String[] parts = line.trim().split("\\s+");
151+
if (parts.length >= 2) {
152+
return new ProcessInfo(parts[1], parts[0]);
153+
}
154+
}
155+
return null;
156+
}
157+
158+
private void waitForProcess(Process process) throws IOException {
159+
try {
160+
if (!process.waitFor(1, TimeUnit.SECONDS)) {
161+
process.destroy();
162+
throw new IOException("Process timed out");
163+
}
164+
}
165+
catch (InterruptedException ex) {
166+
Thread.currentThread().interrupt();
167+
throw new IOException("Process interrupted", ex);
168+
}
169+
}
170+
171+
private List<String> readOutput(Process process) throws IOException {
172+
List<String> lines = new ArrayList<>();
173+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
174+
String line;
175+
while ((line = reader.readLine()) != null) {
176+
lines.add(line);
177+
}
178+
}
179+
return lines;
180+
}
181+
182+
private record ProcessInfo(String pid, String command) {
37183
}
38184

39185
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.diagnostics.analyzer;
18+
19+
import java.net.ServerSocket;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.boot.diagnostics.FailureAnalysis;
24+
import org.springframework.boot.system.ApplicationPid;
25+
import org.springframework.boot.web.server.PortInUseException;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Tests for {@link PortInUseFailureAnalyzer}.
31+
*
32+
* @author Yonatan Graber
33+
*/
34+
class PortInUseFailureAnalyzerTests {
35+
36+
private final PortInUseFailureAnalyzer analyzer = new PortInUseFailureAnalyzer();
37+
38+
@Test
39+
void analyzeNoProcessInfo() throws Exception {
40+
// Best effort attempt to get a port that is not bound to any process
41+
ServerSocket serverSocket = new ServerSocket(0);
42+
int port = serverSocket.getLocalPort();
43+
serverSocket.close();
44+
45+
PortInUseException exception = new PortInUseException(port, null);
46+
FailureAnalysis analysis = this.analyzer.analyze(new RuntimeException("Connection failed", exception),
47+
exception);
48+
49+
assertThat(analysis.getDescription())
50+
.contains("Web server failed to start. Port " + port + " was already in use.");
51+
assertThat(analysis.getAction()).contains("Identify and stop the process that's listening on port " + port
52+
+ " or configure this application to listen on another port.");
53+
assertThat(analysis.getCause()).isSameAs(exception);
54+
}
55+
56+
@Test
57+
void analyzeWithProcessInfo() throws Exception {
58+
// bind a port to this process and check if the analyzer can find it
59+
long pid = new ApplicationPid().toLong();
60+
try (ServerSocket serverSocket = new ServerSocket(0)) {
61+
int port = serverSocket.getLocalPort();
62+
PortInUseException exception = new PortInUseException(port, null);
63+
FailureAnalysis analysis = this.analyzer.analyze(new RuntimeException("Po", exception), exception);
64+
assertThat(analysis.getDescription())
65+
.contains("Web server failed to start. Port " + port + " was already in use ")
66+
.contains("(PID: " + pid + ")");
67+
assertThat(analysis.getAction()).contains("Stop the process ").contains("(PID: " + pid + ")");
68+
}
69+
}
70+
71+
}

0 commit comments

Comments
 (0)