Refined network management (#1672)

This PR implements several refinements to the way that NetworkManager
controls the network interface.

- The monitor detects and logs changes to the network address
- The monitor detects and logs changes to the connection and will
reinitialize the connection if needed
- Remove NetworkInterface.java class, which wasn't used anywhere
- Use java.net.NetworkInterface to get IP addresses for any interface
(device)
- Adds a metric for the current IP address (address on the currently
selected interface)
This commit is contained in:
Craig Schardt
2025-01-03 08:29:18 -06:00
committed by GitHub
parent 7c254ec5dc
commit 474e4f07f8
12 changed files with 273 additions and 204 deletions

View File

@@ -242,7 +242,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<template #activator="{ on, attrs }">
<td v-bind="attrs" v-on="on" @click="setSelectedVideoFormat(value)">
<v-icon small class="mr-2">mdi-information</v-icon>
</td>

View File

@@ -9,24 +9,36 @@ interface MetricItem {
value?: string;
}
const generalMetrics = computed<MetricItem[]>(() => [
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
const generalMetrics = computed<MetricItem[]>(() => {
const stats = [
{
header: "Version",
value: useSettingsStore().general.version || "Unknown"
},
{
header: "Hardware Model",
value: useSettingsStore().general.hardwareModel || "Unknown"
},
{
header: "Platform",
value: useSettingsStore().general.hardwarePlatform || "Unknown"
},
{
header: "GPU Acceleration",
value: useSettingsStore().general.gpuAcceleration || "Unknown"
}
];
if (!useSettingsStore().network.networkingDisabled) {
stats.push({
header: "IP Address",
value: useSettingsStore().metrics.ipAddress || "Unknown"
});
}
]);
return stats;
});
const platformMetrics = computed<MetricItem[]>(() => {
const stats = [

View File

@@ -62,7 +62,8 @@ export const useSettingsStore = defineStore("settings", {
cpuThr: undefined,
cpuUptime: undefined,
diskUtilPct: undefined,
npuUsage: undefined
npuUsage: undefined,
ipAddress: undefined
},
currentFieldLayout: {
field: {
@@ -95,7 +96,8 @@ export const useSettingsStore = defineStore("settings", {
cpuThr: data.cpuThr || undefined,
cpuUptime: data.cpuUptime || undefined,
diskUtilPct: data.diskUtilPct || undefined,
npuUsage: data.npuUsage || undefined
npuUsage: data.npuUsage || undefined,
ipAddress: data.ipAddress || undefined
};
},
updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) {

View File

@@ -23,6 +23,7 @@ export interface MetricData {
cpuUptime?: string;
diskUtilPct?: string;
npuUsage?: string;
ipAddress?: string;
}
export enum NetworkConnectionType {

View File

@@ -167,11 +167,11 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
<v-row>
<!-- Active modules -->
<v-col
v-for="(module, index) in activeVisionModules"
:key="`enabled-${module.uniqueName}`"
cols="12"
sm="6"
lg="4"
v-for="(module, index) in activeVisionModules"
:key="`enabled-${module.uniqueName}`"
>
<v-card dark color="primary">
<v-card-title>{{ module.nickname }}</v-card-title>
@@ -223,8 +223,8 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
</tbody>
</v-simple-table>
<photon-camera-stream
class="mt-3"
id="output-camera-stream"
class="mt-3"
:camera-settings="module"
stream-type="Processed"
style="width: 100%; height: auto"
@@ -233,16 +233,16 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
<v-card-text class="pt-0">
<v-row>
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
<v-btn color="secondary" @click="setCameraView(module.matchedCameraInfo, true)" style="width: 100%">
<v-btn color="secondary" style="width: 100%" @click="setCameraView(module.matchedCameraInfo, true)">
<span>Details</span>
</v-btn>
</v-col>
<v-col cols="6" md="5" class="pr-0">
<v-btn
class="black--text"
@click="deactivateCamera(module.uniqueName)"
color="accent"
style="width: 100%"
@click="deactivateCamera(module.uniqueName)"
>
Deactivate
</v-btn>
@@ -250,9 +250,9 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
<v-col cols="6" md="3">
<v-btn
class="black--text pa-0"
@click="deleteThisCamera(module.uniqueName)"
color="red"
style="width: 100%"
@click="deleteThisCamera(module.uniqueName)"
>
<v-icon>mdi-trash-can-outline</v-icon>
</v-btn>
@@ -263,7 +263,7 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
</v-col>
<!-- Disabled modules -->
<v-col cols="12" sm="6" lg="4" v-for="module in disabledVisionModules" :key="`disabled-${module.uniqueName}`">
<v-col v-for="module in disabledVisionModules" :key="`disabled-${module.uniqueName}`" cols="12" sm="6" lg="4">
<v-card dark color="primary">
<v-card-title>{{ module.nickname }}</v-card-title>
<v-card-subtitle>Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
@@ -299,16 +299,16 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
<v-card-text class="pt-0">
<v-row>
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
<v-btn color="secondary" @click="setCameraView(module.matchedCameraInfo)" style="width: 100%">
<v-btn color="secondary" style="width: 100%" @click="setCameraView(module.matchedCameraInfo)">
<span>Details</span>
</v-btn>
</v-col>
<v-col cols="6" md="5" class="pr-0">
<v-btn
class="black--text"
@click="activateModule(module.uniqueName)"
color="accent"
style="width: 100%"
@click="activateModule(module.uniqueName)"
>
Activate
</v-btn>
@@ -316,9 +316,9 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
<v-col cols="6" md="3">
<v-btn
class="black--text pa-0"
@click="deleteThisCamera(module.uniqueName)"
color="red"
style="width: 100%"
@click="deleteThisCamera(module.uniqueName)"
>
<v-icon>mdi-trash-can-outline</v-icon>
</v-btn>
@@ -329,7 +329,7 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
</v-col>
<!-- Unassigned cameras -->
<v-col cols="12" sm="6" lg="4" v-for="(camera, index) in unmatchedCameras" :key="index">
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4">
<v-card dark color="primary">
<v-card-title>
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
@@ -345,12 +345,12 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
<v-card-text class="pt-0">
<v-row>
<v-col cols="6" class="pr-0">
<v-btn color="secondary" @click="setCameraView(camera)" style="width: 100%">
<v-btn color="secondary" style="width: 100%" @click="setCameraView(camera)">
<span>Details</span>
</v-btn>
</v-col>
<v-col cols="6">
<v-btn class="black--text" @click="activateCamera(camera)" color="accent" style="width: 100%">
<v-btn class="black--text" color="accent" style="width: 100%" @click="activateCamera(camera)">
Activate
</v-btn>
</v-col>
@@ -377,7 +377,7 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
<!-- Camera details modal -->
<v-dialog v-model="viewingDetails">
<v-card dark flat color="primary" v-if="viewingCamera !== null">
<v-card v-if="viewingCamera !== null" dark flat color="primary">
<v-card-title class="d-flex justify-space-between">
<span>{{ cameraInfoFor(viewingCamera)?.name ?? cameraInfoFor(viewingCamera)?.baseName }}</span>
<v-btn text @click="setCameraView(null)">
@@ -398,10 +398,10 @@ const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false
</v-banner>
<div v-if="showCurrentView">
<h3>Saved camera</h3>
<PvCameraInfoCard :camera="viewingCamera" :showTitle="false" />
<PvCameraInfoCard :camera="viewingCamera" :show-title="false" />
<br />
<h3>Current camera</h3>
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" :showTitle="false" />
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" :show-title="false" />
</div>
<div v-else>
<PvCameraInfoCard :camera="viewingCamera" />

View File

@@ -62,8 +62,8 @@ const arducamWarningShown = computed<boolean>(() => {
<template>
<v-container class="pa-3" fluid>
<v-banner
v-model="arducamWarningShown"
v-if="arducamWarningShown"
v-model="arducamWarningShown"
rounded
color="red"
dark
@@ -86,7 +86,7 @@ const arducamWarningShown = computed<boolean>(() => {
<PipelineConfigCard />
<!-- TODO - not sure this belongs here -->
<v-dialog :persistent="false" v-model="warningShown" v-if="warningShown" max-width="800" dark>
<v-dialog v-if="warningShown" v-model="warningShown" :persistent="false" max-width="800" dark>
<v-card dark flat color="primary">
<v-card-title>Setup some cameras to get started!</v-card-title>
<v-card-text>

View File

@@ -20,6 +20,7 @@ package org.photonvision.common.hardware.metrics;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.HardwareConfig;
import org.photonvision.common.dataflow.DataChangeService;
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
@@ -31,6 +32,7 @@ import org.photonvision.common.hardware.metrics.cmds.PiCmds;
import org.photonvision.common.hardware.metrics.cmds.RK3588Cmds;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.networking.NetworkUtils;
import org.photonvision.common.util.ShellExec;
public class MetricsManager {
@@ -119,6 +121,14 @@ public class MetricsManager {
return safeExecute(cmds.ramUsageCommand);
}
public String getIpAddress() {
String dev = ConfigManager.getInstance().getConfig().getNetworkConfig().networkManagerIface;
logger.debug("Requesting IP addresses for \"" + dev + "\"");
String addr = NetworkUtils.getIPAddresses(dev);
logger.debug("Got value \"" + addr + "\"");
return addr;
}
public void publishMetrics() {
logger.debug("Publishing Metrics...");
final var metrics = new HashMap<String, String>();
@@ -133,6 +143,7 @@ public class MetricsManager {
metrics.put("gpuMemUtil", this.getMallocedMemory());
metrics.put("diskUtilPct", this.getUsedDiskPct());
metrics.put("npuUsage", this.getNpuUsage());
metrics.put("ipAddress", this.getIpAddress());
DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics));
}

View File

@@ -1,78 +0,0 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.networking;
import java.net.InterfaceAddress;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
@SuppressWarnings("WeakerAccess")
public class NetworkInterface {
private static final Logger logger = new Logger(NetworkInterface.class, LogGroup.General);
public final String name;
public final String displayName;
public final String ipAddress;
public final String netmask;
public final String broadcast;
public NetworkInterface(java.net.NetworkInterface inetface, InterfaceAddress ifaceAddress) {
name = inetface.getName();
displayName = inetface.getDisplayName();
var inetAddress = ifaceAddress.getAddress();
ipAddress = inetAddress.getHostAddress();
netmask = getIPv4LocalNetMask(ifaceAddress);
// TODO: (low) hack to "get" gateway, this is gross and bad, pls fix
var splitIPAddr = ipAddress.split("\\.");
splitIPAddr[3] = "1";
splitIPAddr[3] = "255";
broadcast = String.join(".", splitIPAddr);
}
private static String getIPv4LocalNetMask(InterfaceAddress interfaceAddress) {
var netPrefix = interfaceAddress.getNetworkPrefixLength();
try {
// Since this is for IPv4, it's 32 bits, so set the sign value of
// the int to "negative"...
int shiftby = (1 << 31);
// For the number of bits of the prefix -1 (we already set the sign bit)
for (int i = netPrefix - 1; i > 0; i--) {
// Shift the sign right... Java makes the sign bit sticky on a shift...
// So no need to "set it back up"...
shiftby = (shiftby >> 1);
}
// Transform the resulting value in xxx.xxx.xxx.xxx format, like if
/// it was a standard address...
// Return the address thus created...
return ((shiftby >> 24) & 255)
+ "."
+ ((shiftby >> 16) & 255)
+ "."
+ ((shiftby >> 8) & 255)
+ "."
+ (shiftby & 255);
// return InetAddress.getByName(maskString);
} catch (Exception e) {
logger.error("Failed to get netmask!", e);
}
// Something went wrong here...
return null;
}
}

View File

@@ -17,11 +17,12 @@
package org.photonvision.common.networking;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.NoSuchElementException;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.DataChangeDestination;
@@ -38,6 +39,7 @@ import org.photonvision.common.util.TimedTaskManager;
public class NetworkManager {
private static final Logger logger = new Logger(NetworkManager.class, LogGroup.General);
private HashMap<String, String> activeConnections = new HashMap<String, String>();
private NetworkManager() {}
@@ -61,17 +63,21 @@ public class NetworkManager {
if (!Platform.isLinux()) {
logger.info("Not managing network on non-Linux platforms.");
this.networkingIsDisabled = true;
return;
}
if (!PlatformUtils.isRoot()) {
logger.error("Cannot manage network without root!");
this.networkingIsDisabled = true;
return;
}
// Start tasks to monitor the network interface(s)
var ethernetDevices = NetworkUtils.getAllWiredInterfaces();
for (NMDeviceInfo deviceInfo : ethernetDevices) {
activeConnections.put(
deviceInfo.devName, NetworkUtils.getActiveConnection(deviceInfo.devName));
monitorDevice(deviceInfo.devName, 5000);
}
@@ -81,7 +87,9 @@ public class NetworkManager {
try {
// if the configured interface isn't in the list of available ones, select one that is
var iFace = physicalDevices.stream().findFirst().orElseThrow();
logger.warn("The configured interface doesn't match any available interface. Applying configuration to " + iFace.devName);
logger.warn(
"The configured interface doesn't match any available interface. Applying configuration to "
+ iFace.devName);
// update NetworkConfig with found interface
config.networkManagerIface = iFace.devName;
ConfigManager.getInstance().requestSave();
@@ -96,7 +104,13 @@ public class NetworkManager {
}
}
logger.info("Setting " + config.connectionType + " with team " + config.ntServerAddress + " on " + config.networkManagerIface);
logger.info(
"Setting "
+ config.connectionType
+ " with team "
+ config.ntServerAddress
+ " on "
+ config.networkManagerIface);
// always set hostname (unless it's blank)
if (!config.hostname.isBlank()) {
@@ -129,15 +143,14 @@ public class NetworkManager {
var shell = new ShellExec(true, false);
shell.executeBashCommand("cat /etc/hostname | tr -d \" \\t\\n\\r\"");
var oldHostname = shell.getOutput().replace("\n", "");
logger.debug("Old host name: >" + oldHostname +"<");
logger.debug("New host name: >" + hostname +"<");
logger.debug("Old host name: \"" + oldHostname + "\"");
logger.debug("New host name: \"" + hostname + "\"");
if (!oldHostname.equals(hostname)) {
var setHostnameRetCode =
shell.executeBashCommand(
"echo $NEW_HOSTNAME > /etc/hostname".replace("$NEW_HOSTNAME", hostname));
setHostnameRetCode =
shell.executeBashCommand("hostnamectl set-hostname " + hostname);
setHostnameRetCode = shell.executeBashCommand("hostnamectl set-hostname " + hostname);
// Add to /etc/hosts
var addHostRetCode =
@@ -168,31 +181,23 @@ public class NetworkManager {
private void setConnectionDHCP(NetworkConfig config) {
String connName = "dhcp-" + config.networkManagerIface;
String addDHCPcommand = """
nmcli connection add
con-name "${connection}"
ifname "${interface}"
type ethernet
autoconnect no
ipv4.method auto
ipv6.method disabled
""";
addDHCPcommand = addDHCPcommand.replaceAll("[\\n]", " ");
var shell = new ShellExec();
try {
if (NetworkUtils.connDoesNotExist(connName)) {
// create connection
logger.info("Creating the DHCP connection " + connName );
logger.info("Creating DHCP connection " + connName);
shell.executeBashCommand(
addDHCPcommand
.replace("${connection}", connName)
.replace("${interface}", config.networkManagerIface)
);
NetworkingCommands.addConnectionCommand
.replace("${connection}", connName)
.replace("${interface}", config.networkManagerIface));
}
logger.info("Updating the DHCP connection " + connName);
shell.executeBashCommand(
NetworkingCommands.modDHCPCommand.replace("${connection}", connName));
// activate it
logger.info("Activating the DHCP connection " + connName );
shell.executeBashCommand("nmcli connection up \"${connection}\"".replace("${connection}", connName), false);
logger.info("Activating DHCP connection " + connName);
shell.executeBashCommand(
"nmcli connection up \"${connection}\"".replace("${connection}", connName), false);
activeConnections.put(config.networkManagerIface, connName);
} catch (Exception e) {
logger.error("Exception while setting DHCP!", e);
}
@@ -200,20 +205,6 @@ public class NetworkManager {
private void setConnectionStatic(NetworkConfig config) {
String connName = "static-" + config.networkManagerIface;
String addStaticCommand = """
nmcli connection add
con-name "${connection}"
ifname "${interface}"
type ethernet
autoconnect no
ipv4.addresses ${ipaddr}/8
ipv4.gateway ${gateway}
ipv4.method "manual"
ipv6.method "disabled"
""";
addStaticCommand = addStaticCommand.replaceAll("[\\n]", " ");
String modStaticCommand = "nmcli connection mod \"${connection}\" ipv4.addresses ${ipaddr}/8 ipv4.gateway ${gateway}";
if (config.staticIp.isBlank()) {
logger.warn("Got empty static IP?");
@@ -222,34 +213,31 @@ public class NetworkManager {
// guess at the gateway from the staticIp
String[] parts = config.staticIp.split("\\.");
parts[parts.length-1] = "1";
parts[parts.length - 1] = "1";
String gateway = String.join(".", parts);
var shell = new ShellExec();
try {
if (NetworkUtils.connDoesNotExist(connName)) {
// create connection
logger.info("Creating the Static connection " + connName );
logger.info("Creating Static connection " + connName);
shell.executeBashCommand(
addStaticCommand
.replace("${connection}", connName)
.replace("${interface}", config.networkManagerIface)
.replace("${ipaddr}", config.staticIp)
.replace("${gateway}", gateway)
);
} else {
// modify it in case the static IP address is different
logger.info("Modifying the Static connection " + connName );
shell.executeBashCommand(
modStaticCommand
.replace("${connection}", connName)
.replace("${ipaddr}", config.staticIp)
.replace("${gateway}", gateway)
);
NetworkingCommands.addConnectionCommand
.replace("${connection}", connName)
.replace("${interface}", config.networkManagerIface));
}
// modify it in case the static IP address is different
logger.info("Updating the Static connection " + connName);
shell.executeBashCommand(
NetworkingCommands.modStaticCommand
.replace("${connection}", connName)
.replace("${ipaddr}", config.staticIp)
.replace("${gateway}", gateway));
// activate it
logger.info("Activating the Static connection " + connName );
shell.executeBashCommand("nmcli connection up \"${connection}\"".replace("${connection}", connName), false);
logger.info("Activating the Static connection " + connName);
shell.executeBashCommand(
"nmcli connection up \"${connection}\"".replace("${connection}", connName), false);
activeConnections.put(config.networkManagerIface, connName);
} catch (Exception e) {
logger.error("Error while setting static IP!", e);
}
@@ -267,31 +255,60 @@ public class NetworkManager {
logger.error("Can't find " + path + ", so can't monitor " + devName);
return;
}
logger.debug("Watching network interface at path: " + path);
var last = new Object() {boolean carrier = true; boolean exceptionLogged = false;};
Runnable task = () -> {
try {
boolean carrier = Files.readString(path).trim().equals("1");
if (carrier != last.carrier) {
if (carrier) {
// carrier came back
logger.info("Interface " + devName + " has re-connected, reinitializing");
reinitialize();
} else {
logger.warn("Interface " + devName + " is disconnected, check Ethernet!");
var last =
new Object() {
boolean carrier = true;
boolean exceptionLogged = false;
String addresses = "";
};
Runnable task =
() -> {
try {
boolean carrier = Files.readString(path).trim().equals("1");
if (carrier != last.carrier) {
if (carrier) {
// carrier came back
logger.info("Interface " + devName + " has re-connected, reinitializing");
reinitialize();
} else {
logger.warn("Interface " + devName + " is disconnected, check Ethernet!");
}
}
NetworkInterface iFace;
iFace = NetworkInterface.getByName(devName);
if (iFace.isUp()) {
String tmpAddresses = "";
tmpAddresses = iFace.getInterfaceAddresses().toString();
if (!last.addresses.equals(tmpAddresses)) {
// addresses have changed, log the difference
last.addresses = tmpAddresses;
logger.info("Interface " + devName + " has address(es): " + last.addresses);
}
var conn = NetworkUtils.getActiveConnection(devName);
if (!conn.equals(activeConnections.get(devName))) {
logger.warn(
"Unexpected connection "
+ conn
+ " active on "
+ devName
+ ". Expected "
+ activeConnections.get(devName));
logger.info("Reinitializing");
reinitialize();
}
}
last.carrier = carrier;
last.exceptionLogged = false;
} catch (Exception e) {
if (!last.exceptionLogged) {
// Log the exception only once, but keep trying
logger.error("Could not check network status for " + devName, e);
last.exceptionLogged = true;
}
}
}
last.carrier = carrier;
last.exceptionLogged = false;
} catch (Exception e) {
if (!last.exceptionLogged) {
// Log the exception only once, but keep trying
logger.error("Could not check network status for " + devName, e);
last.exceptionLogged = true;
}
}
};
};
TimedTaskManager.getInstance().addTask(taskName, task, millisInterval);
logger.debug("Watching network interface at path: " + path);
}
}

View File

@@ -18,6 +18,7 @@
package org.photonvision.common.networking;
import java.io.IOException;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
@@ -156,15 +157,50 @@ public class NetworkUtils {
return null;
}
public static String getActiveConnection(String devName) {
var shell = new ShellExec(true, true);
try {
shell.executeBashCommand(
"nmcli -g GENERAL.CONNECTION dev show \"" + devName + "\"", true, false);
return shell.getOutput().strip();
} catch (Exception e) {
logger.error("Exception from nmcli!");
}
return "";
}
public static boolean connDoesNotExist(String connName) {
var shell = new ShellExec(true, true);
try {
// set nmcli back to DHCP, and re-run dhclient -- this ought to grab a new IP address
shell.executeBashCommand("nmcli -f GENERAL.STATE connection show \"" + connName + "\"");
shell.executeBashCommand(
"nmcli -g GENERAL.STATE connection show \"" + connName + "\"", true, false);
return (shell.getExitCode() == 10);
} catch (Exception e) {
logger.error("Exception from nmcli!");
}
return false;
}
public static String getIPAddresses(String iFaceName) {
if (iFaceName == null || iFaceName.isBlank()) {
return "";
}
List<String> addresses = new ArrayList<String>();
try {
var iFace = NetworkInterface.getByName(iFaceName);
for (var addr : iFace.getInterfaceAddresses()) {
var addrStr = addr.getAddress().toString();
if (addrStr.startsWith("/")) {
addrStr = addrStr.substring(1);
}
addrStr = addrStr + "/" + addr.getNetworkPrefixLength();
addresses.add(addrStr);
}
// addresses = iFace.inetAddresses().map(a ->
// a.getAddress().toString()).collect(Collectors.joining(","));
} catch (Exception e) {
e.printStackTrace();
}
return String.join(", ", addresses);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) Photon Vision.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.photonvision.common.networking;
// using a separate class because Spotless fails on text blocks
// spotless:off
public class NetworkingCommands {
public static final String addConnectionCommand = """
nmcli connection add
con-name "${connection}"
ifname "${interface}"
type ethernet
""".replaceAll("[\\n]", " ");
public static final String modStaticCommand = """
nmcli connection modify ${connection}
autoconnect yes
ipv4.method manual
ipv6.method disabled
ipv4.addresses ${ipaddr}/8
ipv4.gateway ${gateway}
""".replaceAll("[\\n]", " ");
public static final String modDHCPCommand = """
nmcli connection modify "${connection}"
autoconnect yes
ipv4.method auto
ipv6.method disabled
""".replaceAll("[\\n]", " ");
}
//spotless:on

View File

@@ -40,8 +40,15 @@ public class ShellExec {
this.readError = readError;
}
/**
* Execute a bash command. We can handle complex bash commands including multiple executions (; |
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
*
* @param command Bash command to execute
* @return process exit code
*/
public int executeBashCommand(String command) throws IOException {
return executeBashCommand(command, true);
return executeBashCommand(command, true, true);
}
/**
@@ -49,10 +56,25 @@ public class ShellExec {
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
*
* @param command Bash command to execute
* @return true if bash got started, but your command may have failed.
* @param wait true if the command should wait for the proccess to complete
* @return process exit code
*/
public int executeBashCommand(String command, boolean wait) throws IOException {
logger.debug("Executing \"" + command + "\"");
return executeBashCommand(command, true, true);
}
/**
* Execute a bash command. We can handle complex bash commands including multiple executions (; |
* and ||), quotes, expansions ($), escapes (\), e.g.: "cd /abc/def; mv ghi 'older ghi '$(whoami)"
* This runs the commands with the default logging.
*
* @param command Bash command to execute
* @param wait true if the command should wait for the proccess to complete
* @param debug true if the command and return value should be logged
* @return process exit code
*/
public int executeBashCommand(String command, boolean wait, boolean debug) throws IOException {
if (debug) logger.debug("Executing \"" + command + "\"");
boolean success = false;
Runtime r = Runtime.getRuntime();
@@ -68,7 +90,7 @@ public class ShellExec {
// Consume streams, older jvm's had a memory leak if streams were not read,
// some other jvm+OS combinations may block unless streams are consumed.
int retcode = doProcess(wait, process);
logger.debug("Got exit code " + retcode);
if (debug) logger.debug("Got exit code " + retcode);
return retcode;
}