84 changed files with 4188 additions and 648 deletions
@ -0,0 +1,31 @@ |
|||
|
|||
## Running tests in parallel with a reasonable memory usage |
|||
|
|||
```bash |
|||
export MAVEN_OPTS="-Xmx1024m" |
|||
export NODE_OPTIONS="--max_old_space_size=4096" |
|||
export SUREFIRE_JAVA_OPTS="-Xmx1200m -Xss256k -XX:+ExitOnOutOfMemoryError" |
|||
|
|||
mvn clean install -T6 -DskipTests |
|||
mvn test -pl='!application,!dao,!ui-ngx,!msa/js-executor,!msa/web-ui' -T4 |
|||
mvn test -pl dao -Dparallel=packages -DforkCount=4 |
|||
|
|||
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.controller.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 |
|||
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.edge.**' -DforkCount=4 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 |
|||
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.service.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 |
|||
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.mqtt.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 |
|||
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.coap.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 |
|||
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.lwm2m.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 |
|||
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='**/*TestSuite.java' -DforkCount=4 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 |
|||
|
|||
#the rest of application tests |
|||
mvn test -pl application -Dtest=' |
|||
!**/nosql/*Test.java, |
|||
!org.thingsboard.server.controller.**, |
|||
!org.thingsboard.server.edge.**, |
|||
!org.thingsboard.server.service.**, |
|||
!org.thingsboard.server.transport.mqtt.**, |
|||
!org.thingsboard.server.transport.coap.**, |
|||
!org.thingsboard.server.transport.lwm2m.** |
|||
' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5 |
|||
``` |
|||
@ -0,0 +1,204 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.system; |
|||
|
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.MockitoJUnitRunner; |
|||
import org.springframework.security.authentication.BadCredentialsException; |
|||
import org.springframework.security.authentication.DisabledException; |
|||
import org.springframework.security.authentication.LockedException; |
|||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
|||
import org.thingsboard.rule.engine.api.MailService; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.security.UserCredentials; |
|||
import org.thingsboard.server.common.data.security.model.SecuritySettings; |
|||
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; |
|||
import org.thingsboard.server.dao.audit.AuditLogService; |
|||
import org.thingsboard.server.dao.settings.AdminSettingsService; |
|||
import org.thingsboard.server.dao.settings.SecuritySettingsService; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@RunWith(MockitoJUnitRunner.class) |
|||
public class DefaultSystemSecurityServiceTest { |
|||
|
|||
@Mock |
|||
private AdminSettingsService adminSettingsService; |
|||
@Mock |
|||
private BCryptPasswordEncoder encoder; |
|||
@Mock |
|||
private UserService userService; |
|||
@Mock |
|||
private MailService mailService; |
|||
@Mock |
|||
private AuditLogService auditLogService; |
|||
@Mock |
|||
private SecuritySettingsService securitySettingsService; |
|||
|
|||
private DefaultSystemSecurityService systemSecurityService; |
|||
|
|||
private TenantId tenantId; |
|||
private UserId userId; |
|||
private UserCredentials userCredentials; |
|||
private SecuritySettings securitySettings; |
|||
private String username; |
|||
private String password; |
|||
private String encodedPassword; |
|||
|
|||
@Before |
|||
public void setUp() { |
|||
systemSecurityService = new DefaultSystemSecurityService(adminSettingsService, encoder, userService, mailService, auditLogService, securitySettingsService); |
|||
|
|||
tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|||
userId = new UserId(UUID.randomUUID()); |
|||
username = "tenant@example.com"; |
|||
password = "correctPassword"; |
|||
encodedPassword = "$2a$10$encodedPasswordHash"; |
|||
|
|||
userCredentials = new UserCredentials(); |
|||
userCredentials.setUserId(userId); |
|||
userCredentials.setEnabled(true); |
|||
userCredentials.setPassword(encodedPassword); |
|||
userCredentials.setCreatedTime(System.currentTimeMillis()); |
|||
|
|||
securitySettings = new SecuritySettings(); |
|||
securitySettings.setMaxFailedLoginAttempts(5); |
|||
securitySettings.setPasswordPolicy(new UserPasswordPolicy()); |
|||
} |
|||
|
|||
@Test |
|||
public void testValidateUserCredentials_successfulLogin() { |
|||
when(encoder.matches(password, encodedPassword)).thenReturn(true); |
|||
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); |
|||
|
|||
systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password); |
|||
|
|||
verify(encoder).matches(password, encodedPassword); |
|||
verify(userService).resetFailedLoginAttempts(tenantId, userId); |
|||
verify(userService, never()).increaseFailedLoginAttempts(any(), any()); |
|||
} |
|||
|
|||
@Test |
|||
public void testValidateUserCredentials_wrongPassword_incrementsFailedAttempts() { |
|||
when(encoder.matches(password, encodedPassword)).thenReturn(false); |
|||
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(3); |
|||
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); |
|||
|
|||
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) |
|||
.isInstanceOf(BadCredentialsException.class) |
|||
.hasMessageContaining("Authentication Failed"); |
|||
|
|||
verify(userService).increaseFailedLoginAttempts(tenantId, userId); |
|||
verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false)); |
|||
} |
|||
|
|||
@Test |
|||
public void testValidateUserCredentials_wrongPassword_accountLocked() { |
|||
when(encoder.matches(password, encodedPassword)).thenReturn(false); |
|||
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(6); |
|||
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); |
|||
|
|||
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) |
|||
.isInstanceOf(LockedException.class) |
|||
.hasMessageContaining("locked due to security policy"); |
|||
|
|||
verify(userService).increaseFailedLoginAttempts(tenantId, userId); |
|||
verify(userService).setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); |
|||
} |
|||
|
|||
@Test |
|||
public void testValidateUserCredentials_wrongPassword_exactlyAtThreshold_noLock() { |
|||
when(encoder.matches(password, encodedPassword)).thenReturn(false); |
|||
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(5); |
|||
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); |
|||
|
|||
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) |
|||
.isInstanceOf(BadCredentialsException.class) |
|||
.hasMessageContaining("Authentication Failed"); |
|||
|
|||
verify(userService).increaseFailedLoginAttempts(tenantId, userId); |
|||
verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false)); |
|||
} |
|||
|
|||
@Test |
|||
public void testValidateUserCredentials_correctPassword_disabledUser_throwsDisabledException() { |
|||
userCredentials.setEnabled(false); |
|||
|
|||
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) |
|||
.isInstanceOf(DisabledException.class) |
|||
.hasMessage("User is not active"); |
|||
|
|||
verify(encoder, never()).matches(any(), any()); |
|||
verify(userService, never()).increaseFailedLoginAttempts(any(), any()); |
|||
} |
|||
|
|||
@Test |
|||
public void testValidateUserCredentials_wrongPassword_maxAttemptsDisabled() { |
|||
securitySettings.setMaxFailedLoginAttempts(null); |
|||
when(encoder.matches(password, encodedPassword)).thenReturn(false); |
|||
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(100); |
|||
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); |
|||
|
|||
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) |
|||
.isInstanceOf(BadCredentialsException.class); |
|||
|
|||
verify(userService).increaseFailedLoginAttempts(tenantId, userId); |
|||
verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false)); |
|||
} |
|||
|
|||
@Test |
|||
public void testValidateUserCredentials_wrongPassword_maxAttemptsSetToZero() { |
|||
securitySettings.setMaxFailedLoginAttempts(0); |
|||
when(encoder.matches(password, encodedPassword)).thenReturn(false); |
|||
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(100); |
|||
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); |
|||
|
|||
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) |
|||
.isInstanceOf(BadCredentialsException.class); |
|||
|
|||
verify(userService).increaseFailedLoginAttempts(tenantId, userId); |
|||
verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false)); |
|||
} |
|||
|
|||
@Test |
|||
public void testValidateUserCredentials_wrongPassword_withNotificationEmail() throws ThingsboardException { |
|||
String notificationEmail = "admin@example.com"; |
|||
securitySettings.setUserLockoutNotificationEmail(notificationEmail); |
|||
when(encoder.matches(password, encodedPassword)).thenReturn(false); |
|||
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(6); |
|||
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings); |
|||
|
|||
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password)) |
|||
.isInstanceOf(LockedException.class); |
|||
|
|||
verify(userService).setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false); |
|||
verify(mailService).sendAccountLockoutEmail(eq(username), eq(notificationEmail), eq(5)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.utils; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.io.IOException; |
|||
import java.net.DatagramSocket; |
|||
import java.net.SocketException; |
|||
|
|||
@Slf4j |
|||
public class PortFinder { |
|||
public static int findAvailableUdpPort() { |
|||
try (DatagramSocket socket = new DatagramSocket(0)) { |
|||
return socket.getLocalPort(); |
|||
} catch (SocketException e) { |
|||
throw new IllegalStateException("No available UDP ports found", e); |
|||
} |
|||
} |
|||
|
|||
public static boolean isUDPPortAvailable(int port) { |
|||
try (DatagramSocket socket = new DatagramSocket(port)) { |
|||
return true; |
|||
} catch (IOException e) { |
|||
log.debug("Failed to open UDP port {}", port, e); |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -1,154 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.service.timeseries.sql; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.ListeningExecutorService; |
|||
import com.google.common.util.concurrent.MoreExecutors; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.RandomStringUtils; |
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|||
import org.thingsboard.server.common.data.Tenant; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|||
import org.thingsboard.server.common.data.kv.BooleanDataEntry; |
|||
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
|||
import org.thingsboard.server.common.data.kv.LongDataEntry; |
|||
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.dao.service.AbstractServiceTest; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Random; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicLong; |
|||
|
|||
@DaoSqlTest |
|||
@Slf4j |
|||
public class LatestTimeseriesPerformanceTest extends AbstractServiceTest { |
|||
|
|||
private static final String STRING_KEY = "stringKey"; |
|||
private static final String LONG_KEY = "longKey"; |
|||
private static final String DOUBLE_KEY = "doubleKey"; |
|||
private static final String BOOLEAN_KEY = "booleanKey"; |
|||
private static final int AMOUNT_OF_UNIQ_KEY = 10000; |
|||
private static final int TIMEOUT = 100; |
|||
|
|||
private final Random random = new Random(); |
|||
|
|||
@Autowired |
|||
private TimeseriesLatestDao timeseriesLatestDao; |
|||
|
|||
private ListeningExecutorService testExecutor; |
|||
|
|||
private EntityId entityId; |
|||
|
|||
private AtomicLong saveCounter; |
|||
|
|||
@Before |
|||
public void before() { |
|||
Tenant tenant = new Tenant(); |
|||
tenant.setTitle("My tenant"); |
|||
Tenant savedTenant = tenantService.saveTenant(tenant); |
|||
Assert.assertNotNull(savedTenant); |
|||
tenantId = savedTenant.getId(); |
|||
entityId = new DeviceId(UUID.randomUUID()); |
|||
saveCounter = new AtomicLong(0); |
|||
testExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(200, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); |
|||
} |
|||
|
|||
@After |
|||
public void after() { |
|||
tenantService.deleteTenant(tenantId); |
|||
if (testExecutor != null) { |
|||
testExecutor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
public void test_save_latest_timeseries() throws Exception { |
|||
warmup(); |
|||
saveCounter.set(0); |
|||
|
|||
long startTime = System.currentTimeMillis(); |
|||
List<ListenableFuture<?>> futures = new ArrayList<>(); |
|||
for (int i = 0; i < 25_000; i++) { |
|||
futures.add(save(generateStrEntry(getRandomKey()))); |
|||
futures.add(save(generateLngEntry(getRandomKey()))); |
|||
futures.add(save(generateDblEntry(getRandomKey()))); |
|||
futures.add(save(generateBoolEntry(getRandomKey()))); |
|||
} |
|||
Futures.allAsList(futures).get(TIMEOUT, TimeUnit.SECONDS); |
|||
long endTime = System.currentTimeMillis(); |
|||
|
|||
long totalTime = endTime - startTime; |
|||
|
|||
log.info("Total time: {}", totalTime); |
|||
log.info("Saved count: {}", saveCounter.get()); |
|||
log.warn("Saved per 1 sec: {}", saveCounter.get() * 1000 / totalTime); |
|||
} |
|||
|
|||
private void warmup() throws Exception { |
|||
List<ListenableFuture<?>> futures = new ArrayList<>(); |
|||
for (int i = 0; i < AMOUNT_OF_UNIQ_KEY; i++) { |
|||
futures.add(save(generateStrEntry(i))); |
|||
futures.add(save(generateLngEntry(i))); |
|||
futures.add(save(generateDblEntry(i))); |
|||
futures.add(save(generateBoolEntry(i))); |
|||
} |
|||
Futures.allAsList(futures).get(TIMEOUT, TimeUnit.SECONDS); |
|||
} |
|||
|
|||
private ListenableFuture<?> save(TsKvEntry tsKvEntry) { |
|||
return Futures.transformAsync(testExecutor.submit(() -> timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)), result -> { |
|||
saveCounter.incrementAndGet(); |
|||
return result; |
|||
}, testExecutor); |
|||
} |
|||
|
|||
private TsKvEntry generateStrEntry(int keyIndex) { |
|||
return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(STRING_KEY + keyIndex, RandomStringUtils.random(10))); |
|||
} |
|||
|
|||
private TsKvEntry generateLngEntry(int keyIndex) { |
|||
return new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(LONG_KEY + keyIndex, random.nextLong())); |
|||
} |
|||
|
|||
private TsKvEntry generateDblEntry(int keyIndex) { |
|||
return new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(DOUBLE_KEY + keyIndex, random.nextDouble())); |
|||
} |
|||
|
|||
private TsKvEntry generateBoolEntry(int keyIndex) { |
|||
return new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(BOOLEAN_KEY + keyIndex, random.nextBoolean())); |
|||
} |
|||
|
|||
private int getRandomKey() { |
|||
return random.nextInt(AMOUNT_OF_UNIQ_KEY); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.msa; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.io.IOException; |
|||
import java.net.DatagramSocket; |
|||
import java.net.SocketException; |
|||
|
|||
@Slf4j |
|||
public class PortFinder { |
|||
public static int findAvailableUdpPort() { |
|||
try (DatagramSocket socket = new DatagramSocket(0)) { |
|||
return socket.getLocalPort(); |
|||
} catch (SocketException e) { |
|||
throw new IllegalStateException("No available UDP ports found", e); |
|||
} |
|||
} |
|||
|
|||
public static boolean isUDPPortAvailable(int port) { |
|||
try (DatagramSocket socket = new DatagramSocket(port)) { |
|||
return true; |
|||
} catch (IOException e) { |
|||
log.debug("Failed to open UDP port {}", port, e); |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue