Files
VoCat/internal/device/data_linux.go
T
2026-08-09 05:33:21 +08:00

100 lines
3.5 KiB
Go

//go:build linux
package device
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
"vocat/internal/modem"
)
func setQMINetwork(
ctx context.Context,
candidate modem.Candidate,
enabled bool,
apn string,
ipVersion string,
) (NetworkResult, error) {
qmiNetwork, err := exec.LookPath("qmi-network")
if err != nil {
return NetworkResult{}, fmt.Errorf("%w: install libqmi-utils to control %s", ErrDataBackendUnavailable, candidate.QMIControl)
}
profile, err := os.CreateTemp("", "vocat-qmi-*.conf")
if err != nil {
return NetworkResult{}, fmt.Errorf("create temporary QMI profile: %w", err)
}
profilePath := profile.Name()
defer os.Remove(profilePath)
ipType := map[string]string{"IP": "4", "IPV6": "6", "IPV4V6": "4"}[ipVersion]
if _, err := fmt.Fprintf(profile, "APN=%s\nIP_TYPE=%s\nPROXY=yes\n", apn, ipType); err != nil {
_ = profile.Close()
return NetworkResult{}, fmt.Errorf("write temporary QMI profile: %w", err)
}
if err := profile.Chmod(0o600); err != nil {
_ = profile.Close()
return NetworkResult{}, fmt.Errorf("protect temporary QMI profile: %w", err)
}
if err := profile.Close(); err != nil {
return NetworkResult{}, fmt.Errorf("close temporary QMI profile: %w", err)
}
action := "stop"
if enabled {
action = "start"
}
command := exec.CommandContext(ctx, qmiNetwork, "--profile="+profilePath, candidate.QMIControl, action)
output, err := command.CombinedOutput()
detail := strings.TrimSpace(string(output))
if err != nil {
lowerDetail := strings.ToLower(detail)
idempotentStop := !enabled && (strings.Contains(lowerDetail, "already stopped") ||
strings.Contains(lowerDetail, "not started") || strings.Contains(lowerDetail, "no network"))
if !idempotentStop {
return NetworkResult{}, fmt.Errorf("qmi-network %s failed: %w: %s", action, err, detail)
}
}
if ipCommand, lookErr := exec.LookPath("ip"); lookErr == nil {
linkAction := "down"
if enabled {
linkAction = "up"
}
linkOutput, linkErr := exec.CommandContext(ctx, ipCommand, "link", "set", "dev", candidate.NetworkInterface, linkAction).CombinedOutput()
if linkErr != nil {
return NetworkResult{}, fmt.Errorf("set %s %s: %w: %s", candidate.NetworkInterface, linkAction, linkErr, strings.TrimSpace(string(linkOutput)))
}
}
if enabled {
if busybox, lookErr := exec.LookPath("busybox"); lookErr == nil {
dhcpOutput, dhcpErr := exec.CommandContext(ctx, busybox, "udhcpc", "-q", "-n", "-t", "5", "-T", "3", "-i", candidate.NetworkInterface).CombinedOutput()
if dhcpErr != nil {
rollbackCtx, cancelRollback := context.WithTimeout(context.Background(), managerCommandCleanupTimeout)
defer cancelRollback()
_, _ = exec.CommandContext(rollbackCtx, qmiNetwork, "--profile="+profilePath, candidate.QMIControl, "stop").CombinedOutput()
if ipCommand, lookErr := exec.LookPath("ip"); lookErr == nil {
_, _ = exec.CommandContext(rollbackCtx, ipCommand, "link", "set", "dev", candidate.NetworkInterface, "down").CombinedOutput()
}
return NetworkResult{}, fmt.Errorf("QMI session started but DHCP failed: %w: %s", dhcpErr, strings.TrimSpace(string(dhcpOutput)))
}
if value := strings.TrimSpace(string(dhcpOutput)); value != "" {
detail = strings.TrimSpace(detail + "\n" + value)
}
}
}
return NetworkResult{
Enabled: enabled,
Backend: "qmi",
Interface: candidate.NetworkInterface,
ControlDevice: candidate.QMIControl,
APN: apn,
IPVersion: ipVersion,
Detail: detail,
}, nil
}
const managerCommandCleanupTimeout = 15 * time.Second