mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
freebsd: add initial support for running on FreeBSD, excluding Bluez,
ppd, and some things. Add a simple wpa_supplicant backend
This commit is contained in:
@@ -58,3 +58,31 @@ jobs:
|
|||||||
|
|
||||||
- name: Build dankinstall
|
- name: Build dankinstall
|
||||||
run: go build -v ./cmd/dankinstall
|
run: go build -v ./cmd/dankinstall
|
||||||
|
|
||||||
|
build-freebsd:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
- name: Build all packages
|
||||||
|
env:
|
||||||
|
GOOS: freebsd
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Build dms (distropkg, embedded shell)
|
||||||
|
run: |
|
||||||
|
make sync-shell
|
||||||
|
GOOS=freebsd CGO_ENABLED=0 go build -v -tags 'distro_binary withshell' ./cmd/dms
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [amd64, arm64]
|
include:
|
||||||
|
- { goos: linux, arch: amd64 }
|
||||||
|
- { goos: linux, arch: arm64 }
|
||||||
|
- { goos: freebsd, arch: amd64 }
|
||||||
|
- { goos: freebsd, arch: arm64 }
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -35,6 +39,8 @@ jobs:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
|
# linux assets keep their historical arch-only names
|
||||||
|
ASSET: ${{ matrix.goos == 'linux' && matrix.arch || format('{0}-{1}', matrix.goos, matrix.arch) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -50,6 +56,7 @@ jobs:
|
|||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Format check
|
- name: Format check
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
run: |
|
run: |
|
||||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
echo "The following files are not formatted:"
|
echo "The following files are not formatted:"
|
||||||
@@ -58,41 +65,43 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
- name: Build dankinstall (${{ matrix.arch }})
|
- name: Build dankinstall (${{ env.ASSET }})
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dankinstall
|
cd cmd/dankinstall
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dankinstall-${{ matrix.arch }}
|
-o ../../dankinstall-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
gzip -9 -k dankinstall-${{ env.ASSET }}
|
||||||
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Sync embedded shell
|
- name: Sync embedded shell
|
||||||
run: make sync-shell
|
run: make sync-shell
|
||||||
|
|
||||||
- name: Build dms (${{ matrix.arch }})
|
- name: Build dms (${{ env.ASSET }})
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-${{ matrix.arch }}
|
-o ../../dms-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-${{ matrix.arch }}
|
gzip -9 -k dms-${{ env.ASSET }}
|
||||||
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Generate shell completions
|
- name: Generate shell completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
chmod +x dms-amd64
|
chmod +x dms-amd64
|
||||||
@@ -100,46 +109,58 @@ jobs:
|
|||||||
./dms-amd64 completion fish > completion.fish
|
./dms-amd64 completion fish > completion.fish
|
||||||
./dms-amd64 completion zsh > completion.zsh
|
./dms-amd64 completion zsh > completion.zsh
|
||||||
|
|
||||||
- name: Build dms-distropkg (${{ matrix.arch }})
|
- name: Build dms-distropkg (${{ env.ASSET }})
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
-o ../../dms-distropkg-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
gzip -9 -k dms-distropkg-${{ env.ASSET }}
|
||||||
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ env.ASSET }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ env.ASSET }}
|
||||||
path: |
|
path: |
|
||||||
core/dankinstall-${{ matrix.arch }}.gz
|
core/dankinstall-${{ env.ASSET }}.gz
|
||||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-${{ matrix.arch }}.gz
|
core/dms-${{ env.ASSET }}.gz
|
||||||
core/dms-${{ matrix.arch }}.gz.sha256
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload artifacts (${{ env.ASSET }}, no installer)
|
||||||
|
if: matrix.goos != 'linux'
|
||||||
|
uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: core-assets-${{ env.ASSET }}
|
||||||
|
path: |
|
||||||
|
core/dms-${{ env.ASSET }}.gz
|
||||||
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ env.ASSET }}
|
||||||
path: |
|
path: |
|
||||||
core/dankinstall-${{ matrix.arch }}.gz
|
core/dankinstall-${{ env.ASSET }}.gz
|
||||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-${{ matrix.arch }}.gz
|
core/dms-${{ env.ASSET }}.gz
|
||||||
core/dms-${{ matrix.arch }}.gz.sha256
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
core/completion.bash
|
core/completion.bash
|
||||||
core/completion.fish
|
core/completion.fish
|
||||||
core/completion.zsh
|
core/completion.zsh
|
||||||
@@ -248,12 +269,18 @@ jobs:
|
|||||||
### Complete Packages
|
### Complete Packages
|
||||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
- **`dms-full-freebsd-amd64.tar.gz`** - Complete package for FreeBSD x86_64 systems
|
||||||
|
- **`dms-full-freebsd-arm64.tar.gz`** - Complete package for FreeBSD ARM64 systems
|
||||||
|
|
||||||
### Individual Components
|
### Individual Components
|
||||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||||
|
- **`dms-cli-freebsd-amd64.gz`** - DMS CLI binary for FreeBSD x86_64 systems
|
||||||
|
- **`dms-cli-freebsd-arm64.gz`** - DMS CLI binary for FreeBSD ARM64 systems
|
||||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||||
|
- **`dms-distropkg-freebsd-amd64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD x86_64 systems
|
||||||
|
- **`dms-distropkg-freebsd-arm64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD ARM64 systems
|
||||||
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||||
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
||||||
@@ -338,8 +365,8 @@ jobs:
|
|||||||
# Generate checksum for QML package
|
# Generate checksum for QML package
|
||||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||||
|
|
||||||
# Create full packages for each architecture
|
# Create full packages for each os/architecture
|
||||||
for arch in amd64 arm64; do
|
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
|
||||||
mkdir -p _temp_full/dms
|
mkdir -p _temp_full/dms
|
||||||
mkdir -p _temp_full/bin
|
mkdir -p _temp_full/bin
|
||||||
mkdir -p _temp_full/completions
|
mkdir -p _temp_full/completions
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ install-completions:
|
|||||||
@echo "Shell completions installed"
|
@echo "Shell completions installed"
|
||||||
|
|
||||||
install-systemd:
|
install-systemd:
|
||||||
|
ifneq ($(shell uname),Linux)
|
||||||
|
@echo "Skipping systemd user service (non-Linux); start the shell from your compositor config with 'dms run'"
|
||||||
|
else
|
||||||
@echo "Installing systemd user service..."
|
@echo "Installing systemd user service..."
|
||||||
@mkdir -p $(SYSTEMD_USER_DIR)
|
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
|
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
|
||||||
@@ -79,6 +82,7 @@ install-systemd:
|
|||||||
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
|
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||||
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||||
|
endif
|
||||||
|
|
||||||
install-icon:
|
install-icon:
|
||||||
@echo "Installing icon..."
|
@echo "Installing icon..."
|
||||||
|
|||||||
+20
-20
@@ -23,6 +23,14 @@ BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUI
|
|||||||
|
|
||||||
# Architecture to build for dist target (amd64, arm64, or all)
|
# Architecture to build for dist target (amd64, arm64, or all)
|
||||||
ARCH ?= all
|
ARCH ?= all
|
||||||
|
# Target OSes for dist builds
|
||||||
|
DIST_OSES ?= linux freebsd
|
||||||
|
|
||||||
|
ifeq ($(ARCH),all)
|
||||||
|
DIST_ARCHS = amd64 arm64
|
||||||
|
else
|
||||||
|
DIST_ARCHS = $(ARCH)
|
||||||
|
endif
|
||||||
|
|
||||||
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
||||||
|
|
||||||
@@ -57,26 +65,18 @@ dankinstall:
|
|||||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
||||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
||||||
|
|
||||||
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
|
# Build distro binaries (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair
|
||||||
dist: sync-shell
|
dist: sync-shell
|
||||||
ifeq ($(ARCH),all)
|
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..."
|
||||||
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
@echo "Building for linux/amd64..."
|
@for os in $(DIST_OSES); do \
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
|
for arch in $(DIST_ARCHS); do \
|
||||||
@echo "Building for linux/arm64..."
|
echo "Building for $$os/$$arch..."; \
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
|
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch ./$(SOURCE_DIR) || exit 1; \
|
||||||
@echo "Distribution builds complete:"
|
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
|
done; \
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
|
done
|
||||||
else
|
@echo "Distribution builds complete"
|
||||||
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
@echo "Building for linux/$(ARCH)..."
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
|
|
||||||
@echo "Distribution build complete:"
|
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
build-all: build dankinstall
|
build-all: build dankinstall
|
||||||
|
|
||||||
@@ -161,8 +161,8 @@ help:
|
|||||||
@echo " build - Build the main binary (dms) with the embedded UI"
|
@echo " build - Build the main binary (dms) with the embedded UI"
|
||||||
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
|
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
|
||||||
@echo " dankinstall - Build dankinstall binary"
|
@echo " dankinstall - Build dankinstall binary"
|
||||||
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
|
@echo " dist - Build dms for linux/freebsd amd64/arm64 (no update/greeter)"
|
||||||
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
|
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow"
|
||||||
@echo " build-all - Build both binaries"
|
@echo " build-all - Build both binaries"
|
||||||
@echo " install - Install dms to $(INSTALL_DIR)"
|
@echo " install - Install dms to $(INSTALL_DIR)"
|
||||||
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
||||||
|
|||||||
@@ -833,6 +833,8 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
|
|||||||
return "systemd-networkd"
|
return "systemd-networkd"
|
||||||
case network.BackendConnMan:
|
case network.BackendConnMan:
|
||||||
return "ConnMan"
|
return "ConnMan"
|
||||||
|
case network.BackendWpaSupplicant:
|
||||||
|
return "wpa_supplicant"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -980,26 +980,6 @@ func signalTerminals(opts *Options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalByName(name string, sig syscall.Signal) {
|
|
||||||
entries, err := os.ReadDir("/proc")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
pid, err := strconv.Atoi(entry.Name())
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(string(comm)) == name {
|
|
||||||
syscall.Kill(pid, sig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncColorScheme(mode ColorMode) {
|
func syncColorScheme(mode ColorMode) {
|
||||||
scheme := "prefer-dark"
|
scheme := "prefer-dark"
|
||||||
if mode == ColorModeLight {
|
if mode == ColorModeLight {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package matugen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// procfs(5) is optional on FreeBSD; pkill(1) from base queries the kernel
|
||||||
|
// directly.
|
||||||
|
func signalByName(name string, sig syscall.Signal) {
|
||||||
|
signame := strings.TrimPrefix(unix.SignalName(sig), "SIG")
|
||||||
|
exec.Command("pkill", "-"+signame, "-x", name).Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package matugen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func signalByName(name string, sig syscall.Signal) {
|
||||||
|
entries, err := os.ReadDir("/proc")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
pid, err := strconv.Atoi(entry.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(comm)) == name {
|
||||||
|
syscall.Kill(pid, sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,8 @@ const (
|
|||||||
// rest cover distros (or minimal installs) with no /etc/pam.d/login.
|
// rest cover distros (or minimal installs) with no /etc/pam.d/login.
|
||||||
// lockscreenPamBaseDirs mirrors libpam's search order: /etc overrides, then the
|
// lockscreenPamBaseDirs mirrors libpam's search order: /etc overrides, then the
|
||||||
// vendor dir (/usr/lib) and the stateless-distro default (/usr/share).
|
// vendor dir (/usr/lib) and the stateless-distro default (/usr/share).
|
||||||
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d"}
|
// /usr/local/etc/pam.d is OpenPAM's ports dir on FreeBSD (openpam_configure).
|
||||||
|
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d", "/usr/local/etc/pam.d"}
|
||||||
|
|
||||||
// Standalone auth+account services, most universal first. login exists almost
|
// Standalone auth+account services, most universal first. login exists almost
|
||||||
// everywhere (util-linux); system-* cover Fedora/Arch/Gentoo/SUSE-Leap.
|
// everywhere (util-linux); system-* cover Fedora/Arch/Gentoo/SUSE-Leap.
|
||||||
@@ -50,13 +51,15 @@ var lockscreenPamEntryCandidates = []string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for distros with no standalone login service, only shared building
|
// Fallback for distros with no standalone login service, only shared building
|
||||||
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*).
|
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*), FreeBSD
|
||||||
|
// (system holds both stanzas, included by login).
|
||||||
var lockscreenPamSharedIncludePairs = []struct {
|
var lockscreenPamSharedIncludePairs = []struct {
|
||||||
auth string
|
auth string
|
||||||
account string
|
account string
|
||||||
}{
|
}{
|
||||||
{auth: "common-auth", account: "common-account"},
|
{auth: "common-auth", account: "common-account"},
|
||||||
{auth: "base-auth", account: "base-account"},
|
{auth: "base-auth", account: "base-account"},
|
||||||
|
{auth: "system", account: "system"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var includedPamAuthFiles = []string{
|
var includedPamAuthFiles = []string{
|
||||||
@@ -67,6 +70,7 @@ var includedPamAuthFiles = []string{
|
|||||||
"system-local-login",
|
"system-local-login",
|
||||||
"common-auth-pc",
|
"common-auth-pc",
|
||||||
"login",
|
"login",
|
||||||
|
"system",
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthSettings struct {
|
type AuthSettings struct {
|
||||||
@@ -1268,6 +1272,9 @@ func pamModuleExists(module string) bool {
|
|||||||
"/usr/lib/aarch64-linux-gnu/security",
|
"/usr/lib/aarch64-linux-gnu/security",
|
||||||
"/run/current-system/sw/lib64/security",
|
"/run/current-system/sw/lib64/security",
|
||||||
"/run/current-system/sw/lib/security",
|
"/run/current-system/sw/lib/security",
|
||||||
|
"/usr/local/lib/security",
|
||||||
|
"/usr/local/lib",
|
||||||
|
"/usr/lib",
|
||||||
} {
|
} {
|
||||||
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
|
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package brightness
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/dankgo/syncmap"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sys/sys/backlight.h: brightness is a 0-100 percent;
|
||||||
|
// BACKLIGHTGETSTATUS/BACKLIGHTUPDATESTATUS = _IOWR('G', 0/1, struct
|
||||||
|
// backlight_props{uint32 brightness; uint32 nlevels; uint32 levels[100]}).
|
||||||
|
const (
|
||||||
|
backlightDevDir = "/dev/backlight"
|
||||||
|
backlightGetStatus = 0xc1984700
|
||||||
|
backlightUpdateStatus = 0xc1984701
|
||||||
|
)
|
||||||
|
|
||||||
|
type backlightProps struct {
|
||||||
|
brightness uint32
|
||||||
|
nlevels uint32
|
||||||
|
levels [100]uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type BacklightBackend struct {
|
||||||
|
devices syncmap.Map[string, string]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBacklightBackend() (*BacklightBackend, error) {
|
||||||
|
b := &BacklightBackend{}
|
||||||
|
if err := b.scanDevices(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGenericBacklightName(name string) bool {
|
||||||
|
rest, ok := strings.CutPrefix(name, "backlight")
|
||||||
|
if !ok || rest == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range rest {
|
||||||
|
if r < '0' || r > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BacklightBackend) scanDevices() error {
|
||||||
|
entries, err := os.ReadDir(backlightDevDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read %s: %w", backlightDevDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// backlight_register (sys/dev/backlight/backlight.c) publishes each unit
|
||||||
|
// as backlight/backlightN plus a driver-named alias for the same cdev;
|
||||||
|
// dedupe on the device number and keep the descriptive alias.
|
||||||
|
names := make(map[uint64]string)
|
||||||
|
for _, entry := range entries {
|
||||||
|
var st unix.Stat_t
|
||||||
|
if err := unix.Stat(filepath.Join(backlightDevDir, entry.Name()), &st); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rdev := uint64(st.Rdev)
|
||||||
|
current, exists := names[rdev]
|
||||||
|
if exists && !isGenericBacklightName(current) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names[rdev] = entry.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
b.devices.Store("backlight:"+name, filepath.Join(backlightDevDir, name))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func backlightIoctl(fd int, req uint, props *backlightProps) error {
|
||||||
|
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(unsafe.Pointer(props)))
|
||||||
|
if errno != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBrightness(path string) (int, error) {
|
||||||
|
fd, err := unix.Open(path, unix.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer unix.Close(fd)
|
||||||
|
|
||||||
|
var props backlightProps
|
||||||
|
if err := backlightIoctl(fd, backlightGetStatus, &props); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(props.brightness), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BacklightBackend) Rescan() error {
|
||||||
|
return b.scanDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BacklightBackend) GetDevices() ([]Device, error) {
|
||||||
|
devices := make([]Device, 0)
|
||||||
|
|
||||||
|
b.devices.Range(func(id, path string) bool {
|
||||||
|
brightness, err := readBrightness(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to read brightness for %s: %v", id, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
devices = append(devices, Device{
|
||||||
|
Class: ClassBacklight,
|
||||||
|
ID: id,
|
||||||
|
Name: strings.TrimPrefix(id, "backlight:"),
|
||||||
|
Current: brightness,
|
||||||
|
Max: 100,
|
||||||
|
CurrentPercent: brightness,
|
||||||
|
Backend: "backlight",
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return devices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BacklightBackend) SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error {
|
||||||
|
if percent < 0 || percent > 100 {
|
||||||
|
return fmt.Errorf("percent out of range: %d", percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, ok := b.devices.Load(id)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("device not found: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := percent
|
||||||
|
switch {
|
||||||
|
case percent == 0:
|
||||||
|
value = 1
|
||||||
|
case exponential:
|
||||||
|
value = 1 + int(math.Round(math.Pow(float64(percent-1)/99.0, exponent)*99.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := unix.Open(path, unix.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open %s: %w", path, err)
|
||||||
|
}
|
||||||
|
defer unix.Close(fd)
|
||||||
|
|
||||||
|
props := backlightProps{brightness: uint32(value)}
|
||||||
|
if err := backlightIoctl(fd, backlightUpdateStatus, &props); err != nil {
|
||||||
|
return fmt.Errorf("set brightness: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("set %s to %d%% (hw %d) via backlight(4)", id, percent, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
package brightness
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// devd(8) publishes device events on this SOCK_SEQPACKET socket, one
|
||||||
|
// "!system=... subsystem=... type=..." record per packet.
|
||||||
|
const devdSocketPath = "/var/run/devd.seqpacket.pipe"
|
||||||
|
|
||||||
|
const (
|
||||||
|
devdMaxRetries = 5
|
||||||
|
devdBaseDelay = 2 * time.Second
|
||||||
|
devdMaxDelay = 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type DevdMonitor struct {
|
||||||
|
stop chan struct{}
|
||||||
|
rescanMutex sync.Mutex
|
||||||
|
rescanTimer *time.Timer
|
||||||
|
rescanPending bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDevdMonitor(manager *Manager) *DevdMonitor {
|
||||||
|
m := &DevdMonitor{
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
go m.run(manager)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DevdMonitor) run(manager *Manager) {
|
||||||
|
failures := 0
|
||||||
|
for {
|
||||||
|
if err := m.monitorLoop(manager); err != nil {
|
||||||
|
log.Errorf("Devd monitor error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.stop:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
failures++
|
||||||
|
if failures > devdMaxRetries {
|
||||||
|
log.Errorf("Devd monitor exceeded %d retries, giving up", devdMaxRetries)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delay := min(devdBaseDelay*time.Duration(1<<(failures-1)), devdMaxDelay)
|
||||||
|
log.Infof("Devd monitor reconnecting in %v (attempt %d/%d)", delay, failures, devdMaxRetries)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.stop:
|
||||||
|
return
|
||||||
|
case <-time.After(delay):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DevdMonitor) monitorLoop(manager *Manager) error {
|
||||||
|
conn, err := net.Dial("unixpacket", devdSocketPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-m.stop:
|
||||||
|
conn.Close()
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Info("Devd monitor started for backlight/drm events")
|
||||||
|
|
||||||
|
buf := make([]byte, 8192)
|
||||||
|
for {
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-m.stop:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.handleEvent(manager, string(buf[:n]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DevdMonitor) handleEvent(manager *Manager, event string) {
|
||||||
|
notification, ok := strings.CutPrefix(event, "!")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := parseDevdEvent(notification)
|
||||||
|
switch fields["system"] {
|
||||||
|
case "DRM":
|
||||||
|
m.debouncedRescan(manager)
|
||||||
|
case "DEVFS":
|
||||||
|
if fields["subsystem"] != "CDEV" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(fields["cdev"], "backlight/") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.debouncedRescan(manager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDevdEvent(s string) map[string]string {
|
||||||
|
fields := make(map[string]string)
|
||||||
|
for _, part := range strings.Fields(s) {
|
||||||
|
k, v, ok := strings.Cut(part, "=")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields[k] = v
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DevdMonitor) debouncedRescan(manager *Manager) {
|
||||||
|
m.rescanMutex.Lock()
|
||||||
|
defer m.rescanMutex.Unlock()
|
||||||
|
|
||||||
|
m.rescanPending = true
|
||||||
|
|
||||||
|
if m.rescanTimer != nil {
|
||||||
|
m.rescanTimer.Reset(2 * time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
|
||||||
|
m.rescanMutex.Lock()
|
||||||
|
pending := m.rescanPending
|
||||||
|
m.rescanPending = false
|
||||||
|
m.rescanMutex.Unlock()
|
||||||
|
|
||||||
|
if !pending {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.Rescan()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DevdMonitor) Close() {
|
||||||
|
close(m.stop)
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ func NewManagerWithOptions(exponential bool) (*Manager, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go m.initLogind()
|
go m.initLogind()
|
||||||
go m.initSysfs()
|
go m.initNative()
|
||||||
go m.initDDC()
|
go m.initDDC()
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -40,39 +40,6 @@ func (m *Manager) initLogind() {
|
|||||||
log.Info("Logind backend initialized - will use for brightness control")
|
log.Info("Logind backend initialized - will use for brightness control")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) initSysfs() {
|
|
||||||
log.Debug("Initializing sysfs backend...")
|
|
||||||
sysfs, err := NewSysfsBackend()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Failed to initialize sysfs backend: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
devices, err := sysfs.GetDevices()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Failed to get initial sysfs devices: %v", err)
|
|
||||||
m.sysfsBackend = sysfs
|
|
||||||
m.sysfsReady = true
|
|
||||||
m.updateState()
|
|
||||||
m.initUdev()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Sysfs backend initialized with %d devices", len(devices))
|
|
||||||
for _, d := range devices {
|
|
||||||
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.sysfsBackend = sysfs
|
|
||||||
m.sysfsReady = true
|
|
||||||
m.updateState()
|
|
||||||
m.initUdev()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) initUdev() {
|
|
||||||
m.udevMonitor = NewUdevMonitor(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) initDDC() {
|
func (m *Manager) initDDC() {
|
||||||
ddc, err := NewDDCBackend()
|
ddc, err := NewDDCBackend()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -96,9 +63,9 @@ func (m *Manager) Rescan() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.sysfsReady && m.sysfsBackend != nil {
|
if m.nativeReady && m.nativeBackend != nil {
|
||||||
if err := m.sysfsBackend.Rescan(); err != nil {
|
if err := m.nativeBackend.Rescan(); err != nil {
|
||||||
log.Debugf("Sysfs rescan failed: %v", err)
|
log.Debugf("Native backend rescan failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,10 +117,10 @@ func stateChanged(old, new State) bool {
|
|||||||
func (m *Manager) updateState() {
|
func (m *Manager) updateState() {
|
||||||
allDevices := make([]Device, 0)
|
allDevices := make([]Device, 0)
|
||||||
|
|
||||||
if m.sysfsReady && m.sysfsBackend != nil {
|
if m.nativeReady && m.nativeBackend != nil {
|
||||||
devices, err := m.sysfsBackend.GetDevices()
|
devices, err := m.nativeBackend.GetDevices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Failed to get sysfs devices: %v", err)
|
log.Debugf("Failed to get native backend devices: %v", err)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
allDevices = append(allDevices, devices...)
|
allDevices = append(allDevices, devices...)
|
||||||
@@ -232,18 +199,21 @@ func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, expone
|
|||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if deviceClass == ClassDDC {
|
switch {
|
||||||
|
case deviceClass == ClassDDC:
|
||||||
log.Debugf("Calling DDC backend for %s", deviceID)
|
log.Debugf("Calling DDC backend for %s", deviceID)
|
||||||
err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() {
|
err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() {
|
||||||
m.updateState()
|
m.updateState()
|
||||||
m.debouncedBroadcast(deviceID)
|
m.debouncedBroadcast(deviceID)
|
||||||
})
|
})
|
||||||
} else if m.logindReady && m.logindBackend != nil {
|
case m.logindReady && m.logindBackend != nil:
|
||||||
log.Debugf("Calling logind backend for %s", deviceID)
|
log.Debugf("Calling logind backend for %s", deviceID)
|
||||||
err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent)
|
err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent)
|
||||||
} else {
|
case m.nativeBackend != nil:
|
||||||
log.Debugf("Calling sysfs backend for %s", deviceID)
|
log.Debugf("Calling native backend for %s", deviceID)
|
||||||
err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
|
err = m.nativeBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("no brightness backend for %s", deviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package brightness
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Manager) initNative() {
|
||||||
|
log.Debug("Initializing backlight backend...")
|
||||||
|
backend, err := NewBacklightBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to initialize backlight backend: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := backend.GetDevices()
|
||||||
|
if err == nil {
|
||||||
|
log.Infof("Backlight backend initialized with %d devices", len(devices))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.nativeBackend = backend
|
||||||
|
m.nativeReady = true
|
||||||
|
m.updateState()
|
||||||
|
m.monitor = newDevdMonitor(m)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package brightness
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Manager) initNative() {
|
||||||
|
log.Debug("Initializing sysfs backend...")
|
||||||
|
sysfs, err := NewSysfsBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to initialize sysfs backend: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
devices, err := sysfs.GetDevices()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to get initial sysfs devices: %v", err)
|
||||||
|
m.sysfsBackend = sysfs
|
||||||
|
m.nativeBackend = sysfs
|
||||||
|
m.nativeReady = true
|
||||||
|
m.updateState()
|
||||||
|
m.monitor = NewUdevMonitor(m)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Sysfs backend initialized with %d devices", len(devices))
|
||||||
|
for _, d := range devices {
|
||||||
|
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.sysfsBackend = sysfs
|
||||||
|
m.nativeBackend = sysfs
|
||||||
|
m.nativeReady = true
|
||||||
|
m.updateState()
|
||||||
|
m.monitor = NewUdevMonitor(m)
|
||||||
|
}
|
||||||
+5
-4
@@ -43,7 +43,7 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
|
|||||||
logindBackend: mockLogind,
|
logindBackend: mockLogind,
|
||||||
sysfsBackend: sysfs,
|
sysfsBackend: sysfs,
|
||||||
logindReady: true,
|
logindReady: true,
|
||||||
sysfsReady: true,
|
nativeReady: true,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
|
|||||||
logindBackend: mockLogind,
|
logindBackend: mockLogind,
|
||||||
sysfsBackend: sysfs,
|
sysfsBackend: sysfs,
|
||||||
logindReady: true,
|
logindReady: true,
|
||||||
sysfsReady: true,
|
nativeReady: true,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,8 +180,9 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
|
|||||||
m := &Manager{
|
m := &Manager{
|
||||||
logindBackend: nil,
|
logindBackend: nil,
|
||||||
sysfsBackend: sysfs,
|
sysfsBackend: sysfs,
|
||||||
|
nativeBackend: sysfs,
|
||||||
logindReady: false,
|
logindReady: false,
|
||||||
sysfsReady: true,
|
nativeReady: true,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +244,7 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
|
|||||||
logindBackend: mockLogind,
|
logindBackend: mockLogind,
|
||||||
sysfsBackend: sysfs,
|
sysfsBackend: sysfs,
|
||||||
logindReady: true,
|
logindReady: true,
|
||||||
sysfsReady: true,
|
nativeReady: true,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,14 +33,25 @@ type DeviceUpdate struct {
|
|||||||
Device Device `json:"device"`
|
Device Device `json:"device"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Backend interface {
|
||||||
|
Rescan() error
|
||||||
|
GetDevices() ([]Device, error)
|
||||||
|
SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceMonitor interface {
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
logindBackend *LogindBackend
|
logindBackend *LogindBackend
|
||||||
sysfsBackend *SysfsBackend
|
sysfsBackend *SysfsBackend
|
||||||
|
nativeBackend Backend
|
||||||
ddcBackend *DDCBackend
|
ddcBackend *DDCBackend
|
||||||
udevMonitor *UdevMonitor
|
monitor deviceMonitor
|
||||||
|
|
||||||
logindReady bool
|
logindReady bool
|
||||||
sysfsReady bool
|
nativeReady bool
|
||||||
ddcReady bool
|
ddcReady bool
|
||||||
|
|
||||||
exponential bool
|
exponential bool
|
||||||
@@ -170,8 +181,8 @@ func (m *Manager) Close() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if m.udevMonitor != nil {
|
if m.monitor != nil {
|
||||||
m.udevMonitor.Close()
|
m.monitor.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.logindBackend != nil {
|
if m.logindBackend != nil {
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ func setupTestManager(t *testing.T) (*Manager, string) {
|
|||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
sysfsBackend: sysfs,
|
sysfsBackend: sysfs,
|
||||||
sysfsReady: true,
|
nativeReady: true,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default ctrl_interface directory on FreeBSD and minimal Linux setups; see
|
||||||
|
// https://man.freebsd.org/cgi/man.cgi?query=wpa_supplicant.conf&sektion=5
|
||||||
|
const wpaSupplicantCtrlDir = "/var/run/wpa_supplicant"
|
||||||
|
|
||||||
|
type wpaConnectAttempt struct {
|
||||||
|
ssid string
|
||||||
|
netID int
|
||||||
|
saved bool
|
||||||
|
newlyAdded bool
|
||||||
|
prompted bool
|
||||||
|
sawTempDisabled bool
|
||||||
|
finalized bool
|
||||||
|
deadline time.Time
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type WpaSupplicantBackend struct {
|
||||||
|
ctrlDir string
|
||||||
|
ifname string
|
||||||
|
cmd *wpaCtrlConn
|
||||||
|
monitor *wpaCtrlConn
|
||||||
|
state *BackendState
|
||||||
|
stateMutex sync.RWMutex
|
||||||
|
promptBroker PromptBroker
|
||||||
|
onStateChange func()
|
||||||
|
|
||||||
|
stopChan chan struct{}
|
||||||
|
sigWG sync.WaitGroup
|
||||||
|
curAttempt *wpaConnectAttempt
|
||||||
|
attemptMutex sync.RWMutex
|
||||||
|
|
||||||
|
savedIDs map[string]int
|
||||||
|
savedIDsMu sync.Mutex
|
||||||
|
|
||||||
|
recentScans map[string]time.Time
|
||||||
|
recentScansMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWpaSupplicantBackend() (*WpaSupplicantBackend, error) {
|
||||||
|
return &WpaSupplicantBackend{
|
||||||
|
ctrlDir: wpaSupplicantCtrlDir,
|
||||||
|
state: &BackendState{
|
||||||
|
Backend: "wpa_supplicant",
|
||||||
|
WiFiNetworks: []WiFiNetwork{},
|
||||||
|
},
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
savedIDs: make(map[string]int),
|
||||||
|
recentScans: make(map[string]time.Time),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverWpaInterfaces(ctrlDir string) ([]string, error) {
|
||||||
|
entries, err := os.ReadDir(ctrlDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read %s: %w", ctrlDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// wpa_supplicant creates an extra p2p-dev-<ifname> management socket
|
||||||
|
// when built with CONFIG_P2P; it is not a station interface.
|
||||||
|
if strings.HasPrefix(entry.Name(), "p2p-dev-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, entry.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(names)
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) Initialize() error {
|
||||||
|
names, err := discoverWpaInterfaces(b.ctrlDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to discover wpa_supplicant interfaces: %w", err)
|
||||||
|
}
|
||||||
|
if len(names) == 0 {
|
||||||
|
return fmt.Errorf("no wpa_supplicant control sockets in %s", b.ctrlDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ifname = names[0]
|
||||||
|
if len(names) > 1 {
|
||||||
|
log.Infof("wpa_supplicant manages %d interfaces; using %s", len(names), b.ifname)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := newWpaCtrlConn(filepath.Join(b.ctrlDir, b.ifname))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open wpa_ctrl socket: %w", err)
|
||||||
|
}
|
||||||
|
b.cmd = conn
|
||||||
|
|
||||||
|
reply, err := b.cmd.request("PING")
|
||||||
|
if err != nil {
|
||||||
|
b.cmd.close()
|
||||||
|
return fmt.Errorf("wpa_supplicant not responding on %s: %w", b.ifname, err)
|
||||||
|
}
|
||||||
|
if reply != "PONG" {
|
||||||
|
b.cmd.close()
|
||||||
|
return fmt.Errorf("unexpected PING reply from wpa_supplicant: %s", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.stateMutex.Lock()
|
||||||
|
b.state.WiFiDevice = b.ifname
|
||||||
|
b.state.WiFiEnabled = true
|
||||||
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||||
|
log.Warnf("Failed to get initial saved WiFi networks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.updateState(); err != nil {
|
||||||
|
b.cmd.close()
|
||||||
|
return fmt.Errorf("failed to get initial state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := b.updateWiFiNetworks(); err != nil {
|
||||||
|
log.Warnf("Failed to get initial WiFi networks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.ScanWiFi(); err != nil {
|
||||||
|
log.Debugf("Initial WiFi scan not started: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) Close() {
|
||||||
|
b.StopMonitoring()
|
||||||
|
|
||||||
|
if b.monitor != nil {
|
||||||
|
_ = b.monitor.send("DETACH")
|
||||||
|
b.monitor.close()
|
||||||
|
}
|
||||||
|
if b.cmd != nil {
|
||||||
|
b.cmd.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) GetCurrentState() (*BackendState, error) {
|
||||||
|
b.stateMutex.RLock()
|
||||||
|
defer b.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
state := *b.state
|
||||||
|
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
|
||||||
|
state.SavedWiFiNetworks = append([]WiFiNetwork(nil), b.state.SavedWiFiNetworks...)
|
||||||
|
state.EthernetDevices = append([]EthernetDevice(nil), b.state.EthernetDevices...)
|
||||||
|
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
|
||||||
|
state.WiFiDevices = b.getWiFiDevicesLocked()
|
||||||
|
|
||||||
|
return &state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) GetPromptBroker() PromptBroker {
|
||||||
|
return b.promptBroker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) SetPromptBroker(broker PromptBroker) error {
|
||||||
|
if broker == nil {
|
||||||
|
return fmt.Errorf("broker cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.promptBroker = broker
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error {
|
||||||
|
if b.promptBroker == nil {
|
||||||
|
return fmt.Errorf("prompt broker not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.promptBroker.Resolve(token, PromptReply{
|
||||||
|
Secrets: secrets,
|
||||||
|
Save: save,
|
||||||
|
Cancel: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) CancelCredentials(token string) error {
|
||||||
|
if b.promptBroker == nil {
|
||||||
|
return fmt.Errorf("prompt broker not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.promptBroker.Resolve(token, PromptReply{
|
||||||
|
Cancel: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) StopMonitoring() {
|
||||||
|
select {
|
||||||
|
case <-b.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(b.stopChan)
|
||||||
|
}
|
||||||
|
b.sigWG.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) promptForPSK(ssid, reason string) (string, bool) {
|
||||||
|
if b.promptBroker == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-b.stopChan:
|
||||||
|
cancel()
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
token, err := b.promptBroker.Ask(ctx, PromptRequest{
|
||||||
|
SSID: ssid,
|
||||||
|
SettingName: "802-11-wireless-security",
|
||||||
|
Fields: []string{"psk"},
|
||||||
|
Reason: reason,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to request credentials for %s: %v", ssid, err)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := b.promptBroker.Wait(ctx, token)
|
||||||
|
if err != nil || reply.Cancel {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
psk, ok := reply.Secrets["psk"]
|
||||||
|
if !ok || psk == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return psk, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) storeSavedIDs(ids map[string]int) {
|
||||||
|
b.savedIDsMu.Lock()
|
||||||
|
b.savedIDs = ids
|
||||||
|
b.savedIDsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) savedNetworkID(ssid string) (int, bool) {
|
||||||
|
b.savedIDsMu.Lock()
|
||||||
|
defer b.savedIDsMu.Unlock()
|
||||||
|
id, ok := b.savedIDs[ssid]
|
||||||
|
return id, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) seenInRecentScan(ssid string) bool {
|
||||||
|
b.recentScansMu.Lock()
|
||||||
|
defer b.recentScansMu.Unlock()
|
||||||
|
lastSeen, ok := b.recentScans[ssid]
|
||||||
|
return ok && time.Since(lastSeen) < 30*time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) setConnectError(code string) {
|
||||||
|
b.stateMutex.Lock()
|
||||||
|
b.state.IsConnecting = false
|
||||||
|
b.state.ConnectingSSID = ""
|
||||||
|
b.state.LastError = code
|
||||||
|
b.stateMutex.Unlock()
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wpaCtrlRequestTimeout = 5 * time.Second
|
||||||
|
wpaCtrlReadBufferSize = 65536
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client side of the wpa_ctrl protocol: a unix datagram socket bound to a
|
||||||
|
// local path, connected to wpa_supplicant's per-interface socket. See
|
||||||
|
// https://w1.fi/wpa_supplicant/devel/ctrl_iface_page.html
|
||||||
|
type wpaCtrlConn struct {
|
||||||
|
sockPath string
|
||||||
|
localDir string
|
||||||
|
mu sync.Mutex
|
||||||
|
conn *net.UnixConn
|
||||||
|
seq int
|
||||||
|
}
|
||||||
|
|
||||||
|
func wpaCtrlLocalSocketBase() string {
|
||||||
|
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
return os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWpaCtrlConn(sockPath string) (*wpaCtrlConn, error) {
|
||||||
|
localDir, err := os.MkdirTemp(wpaCtrlLocalSocketBase(), "dms-wpa-")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create wpa_ctrl socket dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &wpaCtrlConn{sockPath: sockPath, localDir: localDir}
|
||||||
|
if err := c.dialLocked(); err != nil {
|
||||||
|
os.RemoveAll(localDir)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wpaCtrlConn) dialLocked() error {
|
||||||
|
c.seq++
|
||||||
|
localPath := filepath.Join(c.localDir, fmt.Sprintf("ctrl_%d", c.seq))
|
||||||
|
os.Remove(localPath)
|
||||||
|
|
||||||
|
conn, err := net.DialUnix("unixgram",
|
||||||
|
&net.UnixAddr{Name: localPath, Net: "unixgram"},
|
||||||
|
&net.UnixAddr{Name: c.sockPath, Net: "unixgram"})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dial %s: %w", c.sockPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
c.conn = conn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wpaCtrlConn) reconnectLocked() error {
|
||||||
|
if c.conn != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
c.conn = nil
|
||||||
|
}
|
||||||
|
return c.dialLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wpaCtrlConn) reconnect() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.reconnectLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// request sends a command and waits for its reply, retrying once on a fresh
|
||||||
|
// socket so a wpa_supplicant restart between requests is transparent.
|
||||||
|
func (c *wpaCtrlConn) request(cmd string) (string, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
reply, err := c.roundTripLocked(cmd)
|
||||||
|
if err == nil {
|
||||||
|
return reply, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if rerr := c.reconnectLocked(); rerr != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return c.roundTripLocked(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wpaCtrlConn) roundTripLocked(cmd string) (string, error) {
|
||||||
|
if c.conn == nil {
|
||||||
|
if err := c.dialLocked(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = c.conn.SetDeadline(time.Now().Add(wpaCtrlRequestTimeout))
|
||||||
|
if _, err := c.conn.Write([]byte(cmd)); err != nil {
|
||||||
|
return "", fmt.Errorf("write %q: %w", cmd, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, wpaCtrlReadBufferSize)
|
||||||
|
for {
|
||||||
|
n, err := c.conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read reply for %q: %w", cmd, err)
|
||||||
|
}
|
||||||
|
// Unsolicited event datagrams carry a "<priority>" prefix, command
|
||||||
|
// replies never do (ctrl_iface_page.html, "Control interface data").
|
||||||
|
if n > 0 && buf[0] == '<' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return strings.TrimRight(string(buf[:n]), "\n"), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wpaCtrlConn) send(cmd string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.conn == nil {
|
||||||
|
if err := c.dialLocked(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = c.conn.SetWriteDeadline(time.Now().Add(wpaCtrlRequestTimeout))
|
||||||
|
if _, err := c.conn.Write([]byte(cmd)); err != nil {
|
||||||
|
return fmt.Errorf("write %q: %w", cmd, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wpaCtrlConn) readDatagram(timeout time.Duration) (string, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
conn := c.conn
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return "", fmt.Errorf("wpa_ctrl socket closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
buf := make([]byte, wpaCtrlReadBufferSize)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimRight(string(buf[:n]), "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wpaCtrlConn) attach() error {
|
||||||
|
reply, err := c.request("ATTACH")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reply != "OK" {
|
||||||
|
return fmt.Errorf("ATTACH failed: %s", reply)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wpaCtrlConn) close() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
c.conn = nil
|
||||||
|
}
|
||||||
|
os.RemoveAll(c.localDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWpaCtrlTimeout(err error) bool {
|
||||||
|
netErr, ok := err.(net.Error)
|
||||||
|
return ok && netErr.Timeout()
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func interfaceIPv4(ifname string) string {
|
||||||
|
iface, err := net.InterfaceByName(ifname)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipnet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
|
||||||
|
return ipv4.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksWirelessName(name string) bool {
|
||||||
|
return strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) isWpaManaged(name string) bool {
|
||||||
|
return name == b.ifname
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ethernetDevices() []EthernetDevice {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var devices []EthernetDevice
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if iface.Flags&net.FlagLoopback != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if looksVirtual(iface.Name) || looksWirelessName(iface.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if b.isWpaManaged(iface.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
up := iface.Flags&net.FlagUp != 0
|
||||||
|
running := iface.Flags&net.FlagRunning != 0
|
||||||
|
ip := interfaceIPv4(iface.Name)
|
||||||
|
|
||||||
|
stateStr := "off"
|
||||||
|
switch {
|
||||||
|
case running && ip != "":
|
||||||
|
stateStr = "routable"
|
||||||
|
case running:
|
||||||
|
stateStr = "carrier"
|
||||||
|
case up:
|
||||||
|
stateStr = "no-carrier"
|
||||||
|
}
|
||||||
|
|
||||||
|
devices = append(devices, EthernetDevice{
|
||||||
|
Name: iface.Name,
|
||||||
|
HwAddress: iface.HardwareAddr.String(),
|
||||||
|
State: stateStr,
|
||||||
|
Connected: up && running,
|
||||||
|
IP: ip,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
func wiredConnectionsFromEthernetDevices(devices []EthernetDevice) []WiredConnection {
|
||||||
|
conns := make([]WiredConnection, 0, len(devices))
|
||||||
|
for _, dev := range devices {
|
||||||
|
conns = append(conns, WiredConnection{
|
||||||
|
Path: dbus.ObjectPath("/" + dev.Name),
|
||||||
|
ID: dev.Name,
|
||||||
|
UUID: "wired:" + dev.Name,
|
||||||
|
Type: "ethernet",
|
||||||
|
IsActive: dev.Connected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return conns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) GetEthernetDevices() []EthernetDevice {
|
||||||
|
b.stateMutex.RLock()
|
||||||
|
defer b.stateMutex.RUnlock()
|
||||||
|
return append([]EthernetDevice(nil), b.state.EthernetDevices...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||||
|
return wiredConnectionsFromEthernetDevices(b.ethernetDevices()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) GetWiredNetworkDetails(id string) (*WiredNetworkInfoResponse, error) {
|
||||||
|
ifname := strings.TrimPrefix(id, "wired:")
|
||||||
|
|
||||||
|
iface, err := net.InterfaceByName(ifname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get interface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, _ := iface.Addrs()
|
||||||
|
var ipv4s, ipv6s []string
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipnet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
|
||||||
|
ipv4s = append(ipv4s, ipnet.String())
|
||||||
|
} else if ipv6 := ipnet.IP.To16(); ipv6 != nil {
|
||||||
|
ipv6s = append(ipv6s, ipnet.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WiredNetworkInfoResponse{
|
||||||
|
UUID: id,
|
||||||
|
IFace: ifname,
|
||||||
|
HwAddr: iface.HardwareAddr.String(),
|
||||||
|
IPv4: WiredIPConfig{
|
||||||
|
IPs: ipv4s,
|
||||||
|
},
|
||||||
|
IPv6: WiredIPConfig{
|
||||||
|
IPs: ipv6s,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event strings per WPA_EVENT_* in contrib/wpa/src/common/wpa_ctrl.h
|
||||||
|
// (freebsd-src).
|
||||||
|
const (
|
||||||
|
wpaEventConnected = "CTRL-EVENT-CONNECTED"
|
||||||
|
wpaEventDisconnected = "CTRL-EVENT-DISCONNECTED"
|
||||||
|
wpaEventScanResults = "CTRL-EVENT-SCAN-RESULTS"
|
||||||
|
wpaEventTempDisabled = "CTRL-EVENT-SSID-TEMP-DISABLED"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wpaMonitorReadTimeout = 5 * time.Second
|
||||||
|
wpaMonitorPingInterval = 30 * time.Second
|
||||||
|
wpaMonitorReconnectDelay = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type wpaEvent struct {
|
||||||
|
priority int
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWpaEventLine(line string) (wpaEvent, bool) {
|
||||||
|
if len(line) < 3 || line[0] != '<' {
|
||||||
|
return wpaEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
end := strings.IndexByte(line, '>')
|
||||||
|
if end < 0 {
|
||||||
|
return wpaEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
priority, err := strconv.Atoi(line[1:end])
|
||||||
|
if err != nil {
|
||||||
|
return wpaEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := line[end+1:]
|
||||||
|
name, args, _ := strings.Cut(rest, " ")
|
||||||
|
if name == "" {
|
||||||
|
return wpaEvent{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return wpaEvent{priority: priority, name: name, args: args}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args format: id=%d ssid="%s" auth_failures=%u duration=%d reason=%s, per
|
||||||
|
// wpas_auth_failed in contrib/wpa/wpa_supplicant/wpa_supplicant.c
|
||||||
|
// (freebsd-src); the SSID is printf_encoded inside the quotes.
|
||||||
|
func parseWpaTempDisabled(args string) (ssid string, reason string) {
|
||||||
|
if start := strings.Index(args, `ssid="`); start >= 0 {
|
||||||
|
raw := args[start+len(`ssid="`):]
|
||||||
|
if end := indexUnescapedQuote(raw); end >= 0 {
|
||||||
|
ssid = decodeWpaSSIDText(raw[:end])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range strings.Fields(args) {
|
||||||
|
if value, ok := strings.CutPrefix(field, "reason="); ok {
|
||||||
|
reason = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssid, reason
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexUnescapedQuote(s string) int {
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch s[i] {
|
||||||
|
case '\\':
|
||||||
|
i++
|
||||||
|
case '"':
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) StartMonitoring(onStateChange func()) error {
|
||||||
|
b.onStateChange = onStateChange
|
||||||
|
|
||||||
|
monitor, err := newWpaCtrlConn(filepath.Join(b.ctrlDir, b.ifname))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open wpa_ctrl monitor socket: %w", err)
|
||||||
|
}
|
||||||
|
if err := monitor.attach(); err != nil {
|
||||||
|
monitor.close()
|
||||||
|
return fmt.Errorf("failed to attach wpa_ctrl monitor: %w", err)
|
||||||
|
}
|
||||||
|
b.monitor = monitor
|
||||||
|
|
||||||
|
b.sigWG.Add(1)
|
||||||
|
go b.monitorLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) monitorLoop() {
|
||||||
|
defer b.sigWG.Done()
|
||||||
|
|
||||||
|
lastActivity := time.Now()
|
||||||
|
pingOutstanding := false
|
||||||
|
var pingSent time.Time
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-b.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := b.monitor.readDatagram(wpaMonitorReadTimeout)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
lastActivity = time.Now()
|
||||||
|
if strings.HasPrefix(msg, "<") {
|
||||||
|
b.handleEvent(msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if msg == "PONG" {
|
||||||
|
pingOutstanding = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case isWpaCtrlTimeout(err):
|
||||||
|
if pingOutstanding && time.Since(pingSent) > wpaCtrlRequestTimeout {
|
||||||
|
b.reattachMonitor()
|
||||||
|
pingOutstanding = false
|
||||||
|
lastActivity = time.Now()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !pingOutstanding && time.Since(lastActivity) > wpaMonitorPingInterval {
|
||||||
|
if b.monitor.send("PING") != nil {
|
||||||
|
b.reattachMonitor()
|
||||||
|
lastActivity = time.Now()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pingOutstanding = true
|
||||||
|
pingSent = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
b.reattachMonitor()
|
||||||
|
pingOutstanding = false
|
||||||
|
lastActivity = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) reattachMonitor() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-b.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.monitor.reconnect(); err == nil {
|
||||||
|
if err := b.monitor.attach(); err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-b.stopChan:
|
||||||
|
return
|
||||||
|
case <-time.After(wpaMonitorReconnectDelay):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("wpa_supplicant monitor reattached on %s", b.ifname)
|
||||||
|
|
||||||
|
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||||
|
log.Warnf("failed to refresh saved networks after wpa reattach: %v", err)
|
||||||
|
}
|
||||||
|
if err := b.updateState(); err != nil {
|
||||||
|
log.Warnf("failed to refresh state after wpa reattach: %v", err)
|
||||||
|
}
|
||||||
|
if b.onStateChange != nil {
|
||||||
|
b.onStateChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) handleEvent(raw string) {
|
||||||
|
event, ok := parseWpaEventLine(raw)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch event.name {
|
||||||
|
case wpaEventScanResults:
|
||||||
|
b.handleScanResults()
|
||||||
|
case wpaEventConnected:
|
||||||
|
b.handleConnected()
|
||||||
|
case wpaEventDisconnected:
|
||||||
|
b.handleDisconnected()
|
||||||
|
case wpaEventTempDisabled:
|
||||||
|
b.handleTempDisabled(event.args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) handleScanResults() {
|
||||||
|
if _, err := b.updateWiFiNetworks(); err != nil {
|
||||||
|
log.Warnf("failed to update WiFi networks after scan: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.onStateChange != nil {
|
||||||
|
b.onStateChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) handleConnected() {
|
||||||
|
if err := b.updateState(); err != nil {
|
||||||
|
log.Warnf("failed to update wpa state after connect event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.attemptMutex.RLock()
|
||||||
|
att := b.curAttempt
|
||||||
|
b.attemptMutex.RUnlock()
|
||||||
|
|
||||||
|
b.stateMutex.RLock()
|
||||||
|
currentSSID := b.state.WiFiSSID
|
||||||
|
b.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
if att != nil && att.ssid == currentSSID {
|
||||||
|
b.finalizeAttempt(att, "")
|
||||||
|
b.attemptMutex.Lock()
|
||||||
|
if b.curAttempt == att {
|
||||||
|
b.curAttempt = nil
|
||||||
|
}
|
||||||
|
b.attemptMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||||
|
log.Warnf("failed to refresh saved networks after connect event: %v", err)
|
||||||
|
}
|
||||||
|
if b.onStateChange != nil {
|
||||||
|
b.onStateChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) handleDisconnected() {
|
||||||
|
if err := b.updateState(); err != nil {
|
||||||
|
log.Warnf("failed to update wpa state after disconnect event: %v", err)
|
||||||
|
}
|
||||||
|
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||||
|
log.Warnf("failed to refresh saved networks after disconnect event: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.onStateChange != nil {
|
||||||
|
b.onStateChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) handleTempDisabled(args string) {
|
||||||
|
ssid, reason := parseWpaTempDisabled(args)
|
||||||
|
|
||||||
|
b.attemptMutex.RLock()
|
||||||
|
att := b.curAttempt
|
||||||
|
b.attemptMutex.RUnlock()
|
||||||
|
|
||||||
|
if att == nil || att.ssid != ssid {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
att.mu.Lock()
|
||||||
|
att.sawTempDisabled = true
|
||||||
|
att.mu.Unlock()
|
||||||
|
|
||||||
|
code := errdefs.ErrConnectionFailed
|
||||||
|
// WRONG_KEY is the reason wpas_auth_failed reports for a PSK mismatch
|
||||||
|
// (could_be_psk_mismatch path in contrib/wpa/wpa_supplicant/events.c).
|
||||||
|
if reason == "WRONG_KEY" {
|
||||||
|
code = errdefs.ErrBadCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
b.finalizeAttempt(att, code)
|
||||||
|
|
||||||
|
b.attemptMutex.Lock()
|
||||||
|
if b.curAttempt == att {
|
||||||
|
b.curAttempt = nil
|
||||||
|
}
|
||||||
|
b.attemptMutex.Unlock()
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseWpaScanResults(t *testing.T) {
|
||||||
|
text := "bssid / frequency / signal level / flags / ssid\n" +
|
||||||
|
"02:00:01:02:03:04\t2412\t-45\t[WPA2-PSK-CCMP][ESS]\tHome\n" +
|
||||||
|
"02:00:01:02:03:05\t5180\t-60\t[WPA2-EAP-CCMP][ESS]\tOffice\n" +
|
||||||
|
"02:00:01:02:03:06\t2437\t-70\t[ESS]\tCafe\n" +
|
||||||
|
"02:00:01:02:03:07\t2462\t-80\t[WPA2-PSK-CCMP][ESS]\t\n" +
|
||||||
|
"02:00:01:02:03:08\t2412\t-50\t[WEP][ESS]\tTab\\tNet\n" +
|
||||||
|
"garbage line without tabs\n"
|
||||||
|
|
||||||
|
results := parseWpaScanResults(text)
|
||||||
|
|
||||||
|
assert.Len(t, results, 5)
|
||||||
|
assert.Equal(t, "02:00:01:02:03:04", results[0].BSSID)
|
||||||
|
assert.Equal(t, uint32(2412), results[0].Frequency)
|
||||||
|
assert.Equal(t, -45, results[0].Level)
|
||||||
|
assert.Equal(t, "[WPA2-PSK-CCMP][ESS]", results[0].Flags)
|
||||||
|
assert.Equal(t, "Home", results[0].SSID)
|
||||||
|
assert.Empty(t, results[3].SSID)
|
||||||
|
assert.Equal(t, "Tab\tNet", results[4].SSID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWpaWiFiNetworksFromScanResults(t *testing.T) {
|
||||||
|
results := []wpaScanResult{
|
||||||
|
{BSSID: "02:00:01:02:03:04", Frequency: 2412, Level: -45, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: "Home"},
|
||||||
|
{BSSID: "02:00:01:02:03:05", Frequency: 5180, Level: -80, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: "Home"},
|
||||||
|
{BSSID: "02:00:01:02:03:06", Frequency: 5200, Level: -60, Flags: "[WPA2-EAP-CCMP][ESS]", SSID: "Office"},
|
||||||
|
{BSSID: "02:00:01:02:03:07", Frequency: 2437, Level: -70, Flags: "[ESS]", SSID: "Cafe"},
|
||||||
|
{BSSID: "02:00:01:02:03:08", Frequency: 2462, Level: -50, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
networks := wpaWiFiNetworksFromScanResults(results)
|
||||||
|
|
||||||
|
byNet := make(map[string]WiFiNetwork, len(networks))
|
||||||
|
for _, network := range networks {
|
||||||
|
byNet[network.SSID] = network
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, networks, 3)
|
||||||
|
|
||||||
|
home := byNet["Home"]
|
||||||
|
assert.Equal(t, "02:00:01:02:03:04", home.BSSID, "strongest BSS should win")
|
||||||
|
assert.Equal(t, uint8(55), home.Signal)
|
||||||
|
assert.True(t, home.Secured)
|
||||||
|
assert.False(t, home.Enterprise)
|
||||||
|
assert.Equal(t, uint32(1), home.Channel)
|
||||||
|
|
||||||
|
office := byNet["Office"]
|
||||||
|
assert.True(t, office.Secured)
|
||||||
|
assert.True(t, office.Enterprise)
|
||||||
|
|
||||||
|
cafe := byNet["Cafe"]
|
||||||
|
assert.False(t, cafe.Secured)
|
||||||
|
assert.Equal(t, "infrastructure", cafe.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignalPercentFromDbm(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
dbm int
|
||||||
|
expected uint8
|
||||||
|
}{
|
||||||
|
{-30, 70},
|
||||||
|
{-100, 0},
|
||||||
|
{-110, 0},
|
||||||
|
{0, 100},
|
||||||
|
{10, 100},
|
||||||
|
{-55, 45},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
assert.Equal(t, tc.expected, signalPercentFromDbm(tc.dbm), "dbm=%d", tc.dbm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWpaStatus(t *testing.T) {
|
||||||
|
text := "bssid=02:00:01:02:03:04\n" +
|
||||||
|
"freq=2412\n" +
|
||||||
|
"ssid=Home Network\n" +
|
||||||
|
"id=3\n" +
|
||||||
|
"key_mgmt=WPA2-PSK\n" +
|
||||||
|
"wpa_state=COMPLETED\n" +
|
||||||
|
"ip_address=192.168.1.21\n"
|
||||||
|
|
||||||
|
st := parseWpaStatus(text)
|
||||||
|
|
||||||
|
assert.Equal(t, "COMPLETED", st.wpaState)
|
||||||
|
assert.Equal(t, "Home Network", st.ssid)
|
||||||
|
assert.Equal(t, "02:00:01:02:03:04", st.bssid)
|
||||||
|
assert.Equal(t, "192.168.1.21", st.ipAddress)
|
||||||
|
assert.Equal(t, "WPA2-PSK", st.keyMgmt)
|
||||||
|
assert.Equal(t, 3, st.networkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWpaStatus_Disconnected(t *testing.T) {
|
||||||
|
st := parseWpaStatus("wpa_state=DISCONNECTED\naddress=02:00:01:02:03:04\n")
|
||||||
|
|
||||||
|
assert.Equal(t, "DISCONNECTED", st.wpaState)
|
||||||
|
assert.Empty(t, st.ssid)
|
||||||
|
assert.Equal(t, -1, st.networkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWpaSignalPollRSSI(t *testing.T) {
|
||||||
|
rssi, ok := parseWpaSignalPollRSSI("RSSI=-62\nLINKSPEED=433\nNOISE=9999\nFREQUENCY=5180\n")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, -62, rssi)
|
||||||
|
|
||||||
|
_, ok = parseWpaSignalPollRSSI("FAIL")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWpaListNetworks(t *testing.T) {
|
||||||
|
text := "network id / ssid / bssid / flags\n" +
|
||||||
|
"0\tHome\tany\t[CURRENT]\n" +
|
||||||
|
"1\tOffice\tany\t[DISABLED]\n" +
|
||||||
|
"2\tCafe\tany\t\n" +
|
||||||
|
"3\tFlaky\tany\t[TEMP-DISABLED]\n" +
|
||||||
|
"bogus\tline\tany\t\n"
|
||||||
|
|
||||||
|
networks := parseWpaListNetworks(text)
|
||||||
|
|
||||||
|
assert.Len(t, networks, 4)
|
||||||
|
assert.Equal(t, 0, networks[0].id)
|
||||||
|
assert.Equal(t, "Home", networks[0].ssid)
|
||||||
|
assert.True(t, networks[0].current)
|
||||||
|
assert.False(t, networks[0].disabled)
|
||||||
|
|
||||||
|
assert.True(t, networks[1].disabled)
|
||||||
|
assert.False(t, networks[2].disabled)
|
||||||
|
assert.False(t, networks[2].current)
|
||||||
|
assert.True(t, networks[3].tempDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWpaEventLine(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
line string
|
||||||
|
ok bool
|
||||||
|
priority int
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
}{
|
||||||
|
{"<2>CTRL-EVENT-CONNECTED - Connection to 02:00:01:02:03:04 completed [id=0 id_str=]", true, 2, "CTRL-EVENT-CONNECTED", "- Connection to 02:00:01:02:03:04 completed [id=0 id_str=]"},
|
||||||
|
{"<3>CTRL-EVENT-DISCONNECTED bssid=02:00:01:02:03:04 reason=3 locally_generated=1", true, 3, "CTRL-EVENT-DISCONNECTED", "bssid=02:00:01:02:03:04 reason=3 locally_generated=1"},
|
||||||
|
{"<2>CTRL-EVENT-SCAN-RESULTS ", true, 2, "CTRL-EVENT-SCAN-RESULTS", ""},
|
||||||
|
{"OK", false, 0, "", ""},
|
||||||
|
{"", false, 0, "", ""},
|
||||||
|
{"<x>BROKEN", false, 0, "", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
event, ok := parseWpaEventLine(tc.line)
|
||||||
|
assert.Equal(t, tc.ok, ok, "line=%q", tc.line)
|
||||||
|
if !tc.ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.priority, event.priority, "line=%q", tc.line)
|
||||||
|
assert.Equal(t, tc.name, event.name, "line=%q", tc.line)
|
||||||
|
assert.Equal(t, tc.args, event.args, "line=%q", tc.line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWpaTempDisabled(t *testing.T) {
|
||||||
|
ssid, reason := parseWpaTempDisabled(`id=1 ssid="Home Net" auth_failures=1 duration=10 reason=WRONG_KEY`)
|
||||||
|
assert.Equal(t, "Home Net", ssid)
|
||||||
|
assert.Equal(t, "WRONG_KEY", reason)
|
||||||
|
|
||||||
|
ssid, reason = parseWpaTempDisabled(`id=2 ssid="quo\"te" auth_failures=3 duration=20 reason=CONN_FAILED`)
|
||||||
|
assert.Equal(t, `quo"te`, ssid)
|
||||||
|
assert.Equal(t, "CONN_FAILED", reason)
|
||||||
|
|
||||||
|
ssid, reason = parseWpaTempDisabled("garbage")
|
||||||
|
assert.Empty(t, ssid)
|
||||||
|
assert.Empty(t, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeWpaSSIDText(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
encoded string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"Plain", "Plain"},
|
||||||
|
{`With\tTab`, "With\tTab"},
|
||||||
|
{`With\\Backslash`, `With\Backslash`},
|
||||||
|
{`With\"Quote`, `With"Quote`},
|
||||||
|
{`Hex\xc3\xa9`, "Hexé"},
|
||||||
|
{`Trailing\`, `Trailing\`},
|
||||||
|
{`Bad\xzz`, `Bad\xzz`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
assert.Equal(t, tc.expected, decodeWpaSSIDText(tc.encoded), "encoded=%q", tc.encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWpaQuotedString(t *testing.T) {
|
||||||
|
quoted, err := wpaQuotedString("passphrase")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `"passphrase"`, quoted)
|
||||||
|
|
||||||
|
_, err = wpaQuotedString(`with"quote`)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = wpaQuotedString("with\nnewline")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseWpaConfigPassphrase(t *testing.T) {
|
||||||
|
config := "ctrl_interface=/var/run/wpa_supplicant\n" +
|
||||||
|
"update_config=1\n" +
|
||||||
|
"\n" +
|
||||||
|
"network={\n" +
|
||||||
|
"\tssid=\"Home\"\n" +
|
||||||
|
"\tpsk=\"secret pass\"\n" +
|
||||||
|
"}\n" +
|
||||||
|
"\n" +
|
||||||
|
"network={\n" +
|
||||||
|
"\tssid=486578\n" +
|
||||||
|
"\tpsk=\"hexpass\"\n" +
|
||||||
|
"}\n" +
|
||||||
|
"\n" +
|
||||||
|
"network={\n" +
|
||||||
|
"\tssid=\"RawKey\"\n" +
|
||||||
|
"\tpsk=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n" +
|
||||||
|
"}\n"
|
||||||
|
|
||||||
|
pass, err := parseWpaConfigPassphrase(config, "Home")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "secret pass", pass)
|
||||||
|
|
||||||
|
pass, err = parseWpaConfigPassphrase(config, "Hex")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "hexpass", pass)
|
||||||
|
|
||||||
|
_, err = parseWpaConfigPassphrase(config, "RawKey")
|
||||||
|
assert.Error(t, err, "raw hex PSK is not a passphrase")
|
||||||
|
|
||||||
|
_, err = parseWpaConfigPassphrase(config, "Unknown")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startFakeWpaSocket(t *testing.T, handler func(cmd string) []string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
sockPath := filepath.Join(t.TempDir(), "wlan0")
|
||||||
|
conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: sockPath, Net: "unixgram"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listen fake wpa socket: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { conn.Close() })
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
n, addr, err := conn.ReadFromUnix(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, reply := range handler(string(buf[:n])) {
|
||||||
|
conn.WriteToUnix([]byte(reply), addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return sockPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWpaCtrlConn_Request(t *testing.T) {
|
||||||
|
sockPath := startFakeWpaSocket(t, func(cmd string) []string {
|
||||||
|
switch cmd {
|
||||||
|
case "PING":
|
||||||
|
return []string{"PONG\n"}
|
||||||
|
case "SCAN":
|
||||||
|
return []string{"<2>CTRL-EVENT-SCAN-STARTED ", "OK\n"}
|
||||||
|
default:
|
||||||
|
return []string{"FAIL\n"}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
conn, err := newWpaCtrlConn(sockPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open wpa_ctrl conn: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.close()
|
||||||
|
|
||||||
|
reply, err := conn.request("PING")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "PONG", reply)
|
||||||
|
|
||||||
|
reply, err = conn.request("SCAN")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "OK", reply, "unsolicited event datagrams must be skipped")
|
||||||
|
|
||||||
|
reply, err = conn.request("BOGUS")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "FAIL", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWpaSupplicantBackend_ClassifyAttempt(t *testing.T) {
|
||||||
|
backend, _ := NewWpaSupplicantBackend()
|
||||||
|
|
||||||
|
att := &wpaConnectAttempt{ssid: "Home", sawTempDisabled: true}
|
||||||
|
assert.Equal(t, "bad-credentials", backend.classifyAttempt(att))
|
||||||
|
|
||||||
|
att = &wpaConnectAttempt{ssid: "Home"}
|
||||||
|
assert.Equal(t, "no-such-ssid", backend.classifyAttempt(att))
|
||||||
|
|
||||||
|
backend.recentScansMu.Lock()
|
||||||
|
backend.recentScans["Home"] = time.Now()
|
||||||
|
backend.recentScansMu.Unlock()
|
||||||
|
assert.Equal(t, "assoc-timeout", backend.classifyAttempt(att))
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ConnectEthernet() error {
|
||||||
|
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) DisconnectEthernet() error {
|
||||||
|
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) DisconnectEthernetDevice(device string) error {
|
||||||
|
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ActivateWiredConnection(uuid string) error {
|
||||||
|
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
||||||
|
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ListActiveVPN() ([]VPNActive, error) {
|
||||||
|
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||||
|
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) DisconnectVPN(uuidOrName string) error {
|
||||||
|
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) DisconnectAllVPN() error {
|
||||||
|
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ClearVPNCredentials(uuidOrName string) error {
|
||||||
|
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ListVPNPlugins() ([]VPNPlugin, error) {
|
||||||
|
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ImportVPN(filePath string, name string) (*VPNImportResult, error) {
|
||||||
|
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) GetVPNConfig(uuidOrName string) (*VPNConfig, error) {
|
||||||
|
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) UpdateVPNConfig(uuid string, updates map[string]any) error {
|
||||||
|
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) DeleteVPN(uuidOrName string) error {
|
||||||
|
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) SetVPNCredentials(uuid, username, password string, save bool) error {
|
||||||
|
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) ScanWiFiDevice(device string) error {
|
||||||
|
return b.ScanWiFi()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) DisconnectWiFiDevice(device string) error {
|
||||||
|
return b.DisconnectWiFi()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) GetWiFiDevices() []WiFiDevice {
|
||||||
|
b.stateMutex.RLock()
|
||||||
|
defer b.stateMutex.RUnlock()
|
||||||
|
return b.getWiFiDevicesLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *WpaSupplicantBackend) getWiFiDevicesLocked() []WiFiDevice {
|
||||||
|
if b.state.WiFiDevice == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stateStr := "disconnected"
|
||||||
|
if b.state.WiFiConnected {
|
||||||
|
stateStr = "connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
return []WiFiDevice{{
|
||||||
|
Name: b.state.WiFiDevice,
|
||||||
|
State: stateStr,
|
||||||
|
Connected: b.state.WiFiConnected,
|
||||||
|
SSID: b.state.WiFiSSID,
|
||||||
|
BSSID: b.state.WiFiBSSID,
|
||||||
|
Signal: b.state.WiFiSignal,
|
||||||
|
IP: b.state.WiFiIP,
|
||||||
|
Networks: b.state.WiFiNetworks,
|
||||||
|
}}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ package network
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
@@ -14,6 +15,7 @@ const (
|
|||||||
BackendIwd
|
BackendIwd
|
||||||
BackendConnMan
|
BackendConnMan
|
||||||
BackendNetworkd
|
BackendNetworkd
|
||||||
|
BackendWpaSupplicant
|
||||||
)
|
)
|
||||||
|
|
||||||
func nameHasOwner(bus *dbus.Conn, name string) (bool, error) {
|
func nameHasOwner(bus *dbus.Conn, name string) (bool, error) {
|
||||||
@@ -35,10 +37,24 @@ type DetectResult struct {
|
|||||||
ChosenReason string
|
ChosenReason string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wpaSupplicantCtrlDirPresent() bool {
|
||||||
|
info, err := os.Stat(wpaSupplicantCtrlDir)
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
func DetectNetworkStack() (*DetectResult, error) {
|
func DetectNetworkStack() (*DetectResult, error) {
|
||||||
bus, err := dbus.ConnectSystemBus()
|
bus, err := dbus.ConnectSystemBus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("connect system bus: %w", err)
|
// FreeBSD and minimal Linux systems may run wpa_supplicant with no
|
||||||
|
// system bus at all; the control directory is the only signal there.
|
||||||
|
if !wpaSupplicantCtrlDirPresent() {
|
||||||
|
return nil, fmt.Errorf("connect system bus: %w", err)
|
||||||
|
}
|
||||||
|
return &DetectResult{
|
||||||
|
Backend: BackendWpaSupplicant,
|
||||||
|
HasWpaSupp: true,
|
||||||
|
ChosenReason: "System bus unreachable; wpa_supplicant control directory present. Using wpa_ctrl interface.",
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
defer bus.Close()
|
defer bus.Close()
|
||||||
|
|
||||||
@@ -76,10 +92,14 @@ func DetectNetworkStack() (*DetectResult, error) {
|
|||||||
case hasNetworkd:
|
case hasNetworkd:
|
||||||
res.Backend = BackendNetworkd
|
res.Backend = BackendNetworkd
|
||||||
res.ChosenReason = "systemd-networkd detected (no NM/ConnMan). Using networkd for L3 and wired."
|
res.ChosenReason = "systemd-networkd detected (no NM/ConnMan). Using networkd for L3 and wired."
|
||||||
|
case wpaSupplicantCtrlDirPresent():
|
||||||
|
res.Backend = BackendWpaSupplicant
|
||||||
|
res.HasWpaSupp = true
|
||||||
|
res.ChosenReason = "No NM/ConnMan/iwd/networkd; wpa_supplicant control directory present. Using wpa_ctrl interface."
|
||||||
default:
|
default:
|
||||||
res.Backend = BackendNone
|
res.Backend = BackendNone
|
||||||
if hasWpa {
|
if hasWpa {
|
||||||
res.ChosenReason = "No NM/ConnMan/iwd; wpa_supplicant present. Consider a wpa_supplicant path."
|
res.ChosenReason = "No NM/ConnMan/iwd; wpa_supplicant D-Bus name present but control directory missing."
|
||||||
} else {
|
} else {
|
||||||
res.ChosenReason = "No known network manager bus names found."
|
res.ChosenReason = "No known network manager bus names found."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ func TestBackendType_Constants(t *testing.T) {
|
|||||||
assert.Equal(t, BackendType(2), BackendIwd)
|
assert.Equal(t, BackendType(2), BackendIwd)
|
||||||
assert.Equal(t, BackendType(3), BackendConnMan)
|
assert.Equal(t, BackendType(3), BackendConnMan)
|
||||||
assert.Equal(t, BackendType(4), BackendNetworkd)
|
assert.Equal(t, BackendType(4), BackendNetworkd)
|
||||||
|
assert.Equal(t, BackendType(5), BackendWpaSupplicant)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectResult_HasNetworkdField(t *testing.T) {
|
func TestDetectResult_HasNetworkdField(t *testing.T) {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ErrNoNetworkBackend is returned when no supported network management daemon
|
// ErrNoNetworkBackend is returned when no supported network management daemon
|
||||||
// (NetworkManager, iwd, systemd-networkd, ConnMan) is present on the system.
|
// (NetworkManager, iwd, systemd-networkd, ConnMan, wpa_supplicant) is present
|
||||||
|
// on the system.
|
||||||
var ErrNoNetworkBackend = errors.New("no supported network backend found")
|
var ErrNoNetworkBackend = errors.New("no supported network backend found")
|
||||||
|
|
||||||
func NewManager() (*Manager, error) {
|
func NewManager() (*Manager, error) {
|
||||||
@@ -62,6 +63,13 @@ func NewManager() (*Manager, error) {
|
|||||||
backend = nd
|
backend = nd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case BackendWpaSupplicant:
|
||||||
|
wpa, err := NewWpaSupplicantBackend()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create wpa_supplicant backend: %w", err)
|
||||||
|
}
|
||||||
|
backend = wpa
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", ErrNoNetworkBackend, detection.ChosenReason)
|
return nil, fmt.Errorf("%w: %s", ErrNoNetworkBackend, detection.ChosenReason)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const resumeDelay = 3 * time.Second
|
const resumeDelay = 3 * time.Second
|
||||||
@@ -95,21 +94,3 @@ func (m *Manager) Close() {
|
|||||||
}
|
}
|
||||||
log.Info("TrayRecovery manager closed")
|
log.Info("TrayRecovery manager closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// timeSuspended returns how long the system has spent in suspend since boot.
|
|
||||||
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
|
|
||||||
// CLOCK_MONOTONIC (excludes suspend).
|
|
||||||
func timeSuspended() time.Duration {
|
|
||||||
var bt, mt unix.Timespec
|
|
||||||
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
|
|
||||||
if diff < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return time.Duration(diff)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package trayrecovery
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// FreeBSD has no CLOCK_BOOTTIME (clock_gettime(2)), so suspend time since
|
||||||
|
// boot cannot be measured; the startup recovery scan is skipped.
|
||||||
|
func timeSuspended() time.Duration {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package trayrecovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// timeSuspended returns how long the system has spent in suspend since boot.
|
||||||
|
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
|
||||||
|
// CLOCK_MONOTONIC (excludes suspend).
|
||||||
|
func timeSuspended() time.Duration {
|
||||||
|
var bt, mt unix.Timespec
|
||||||
|
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
|
||||||
|
if diff < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return time.Duration(diff)
|
||||||
|
}
|
||||||
@@ -13,12 +13,12 @@ import (
|
|||||||
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const animKelvinStep = 25
|
const animKelvinStep = 25
|
||||||
@@ -920,7 +920,7 @@ func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
|
|||||||
return fmt.Errorf("gamma control invalid")
|
return fmt.Errorf("gamma control invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
fd, err := MemfdCreate("gamma-ramp", 0)
|
fd, err := shm.CreateAnonFd("gamma-ramp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1295,11 +1295,3 @@ func (m *Manager) Close() {
|
|||||||
m.dbusConn.Close()
|
m.dbusConn.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MemfdCreate(name string, flags int) (int, error) {
|
|
||||||
fd, err := unix.MemfdCreate(name, flags)
|
|
||||||
if err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
return fd, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package trash
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
// readMountPoints returns user-visible mount points via getfsstat(2),
|
||||||
|
// skipping pseudo and system filesystems.
|
||||||
|
func readMountPoints() []string {
|
||||||
|
n, err := unix.Getfsstat(nil, unix.MNT_NOWAIT)
|
||||||
|
if err != nil || n == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
stats := make([]unix.Statfs_t, n)
|
||||||
|
n, err = unix.Getfsstat(stats, unix.MNT_NOWAIT)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []string
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, st := range stats[:n] {
|
||||||
|
mp := unix.ByteSliceToString(st.Mntonname[:])
|
||||||
|
if mp == "" || skipMountPoint(mp, seen) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[mp] = true
|
||||||
|
out = append(out, mp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package trash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readMountPoints returns user-visible mount points from /proc/self/mountinfo,
|
||||||
|
// skipping pseudo and system filesystems.
|
||||||
|
func readMountPoints() []string {
|
||||||
|
data, err := os.ReadFile("/proc/self/mountinfo")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for line := range strings.SplitSeq(string(data), "\n") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mp := fields[4]
|
||||||
|
if skipMountPoint(mp, seen) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[mp] = true
|
||||||
|
out = append(out, mp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -275,39 +275,16 @@ func allTrashDirs() []string {
|
|||||||
return dirs
|
return dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
// readMountPoints returns user-visible mount points from /proc/self/mountinfo,
|
func skipMountPoint(mp string, seen map[string]bool) bool {
|
||||||
// skipping pseudo and system filesystems.
|
if mp == "/" || seen[mp] {
|
||||||
func readMountPoints() []string {
|
return true
|
||||||
data, err := os.ReadFile("/proc/self/mountinfo")
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
skipPrefixes := []string{"/proc", "/sys", "/dev"}
|
for _, p := range []string{"/proc", "/sys", "/dev"} {
|
||||||
var out []string
|
if mp == p || strings.HasPrefix(mp, p+"/") {
|
||||||
seen := map[string]bool{}
|
return true
|
||||||
for line := range strings.SplitSeq(string(data), "\n") {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) < 5 {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
mp := fields[4]
|
|
||||||
if mp == "/" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
skip := false
|
|
||||||
for _, p := range skipPrefixes {
|
|
||||||
if mp == p || strings.HasPrefix(mp, p+"/") {
|
|
||||||
skip = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if skip || seen[mp] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[mp] = true
|
|
||||||
out = append(out, mp)
|
|
||||||
}
|
}
|
||||||
return out
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func List() ([]Entry, error) {
|
func List() ([]Entry, error) {
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ type Buffer struct {
|
|||||||
func CreateBuffer(width, height, stride int) (*Buffer, error) {
|
func CreateBuffer(width, height, stride int) (*Buffer, error) {
|
||||||
size := stride * height
|
size := stride * height
|
||||||
|
|
||||||
fd, err := unix.MemfdCreate("dms-shm", 0)
|
fd, err := CreateAnonFd("dms-shm")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("memfd_create: %w", err)
|
return nil, fmt.Errorf("create shm fd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := unix.Ftruncate(fd, int64(size)); err != nil {
|
if err := unix.Ftruncate(fd, int64(size)); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package shm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mirrors FreeBSD libc memfd_create (lib/libc/gen/memfd_create.c):
|
||||||
|
// shm_open2(SHM_ANON, O_RDWR, 0, SHM_GROW_ON_WRITE, name).
|
||||||
|
// x/sys/unix has no wrapper for SYS_shm_open2 (571, sys/sys/syscall.h).
|
||||||
|
const (
|
||||||
|
sysShmOpen2 = 571
|
||||||
|
shmAnon = 1 // SHM_ANON ((char *)1), sys/sys/mman.h
|
||||||
|
shmGrowOnWrite = 0x2 // SHM_GROW_ON_WRITE, sys/sys/mman.h
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateAnonFd(name string) (int, error) {
|
||||||
|
p, err := unix.BytePtrFromString(name)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
fd, _, errno := unix.Syscall6(sysShmOpen2, shmAnon, unix.O_RDWR, 0, shmGrowOnWrite, uintptr(unsafe.Pointer(p)), 0)
|
||||||
|
if errno != 0 {
|
||||||
|
return -1, errno
|
||||||
|
}
|
||||||
|
return int(fd), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package shm
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
func CreateAnonFd(name string) (int, error) {
|
||||||
|
return unix.MemfdCreate(name, 0)
|
||||||
|
}
|
||||||
+1
-1
Submodule dank-qml-common updated: 4fa313766a...493b609cc0
Generated
+3
-3
@@ -3,11 +3,11 @@
|
|||||||
"dank-qml-common": {
|
"dank-qml-common": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784386873,
|
"lastModified": 1784471445,
|
||||||
"narHash": "sha256-n0LKLirPnNSXYxElK1IpnybEwU0nnyso4kpsEFX88U0=",
|
"narHash": "sha256-hRTmcd3Ms9DF3X35ZiapXM15rG5X1DtIdlrQZwRINFE=",
|
||||||
"owner": "AvengeMedia",
|
"owner": "AvengeMedia",
|
||||||
"repo": "dank-qml-common",
|
"repo": "dank-qml-common",
|
||||||
"rev": "4fa313766ab5cd2766be0f87a163305ca9af6faf",
|
"rev": "493b609cc04659f1388f5738e8f65d6c531994fe",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ done
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
// charge_control_* live in Linux sysfs; no BSD equivalent
|
||||||
|
visible: Qt.platform.os === "linux"
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: applyButton.height
|
height: applyButton.height
|
||||||
layoutDirection: I18n.isRtl ? Qt.LeftToRight : Qt.RightToLeft
|
layoutDirection: I18n.isRtl ? Qt.LeftToRight : Qt.RightToLeft
|
||||||
|
|||||||
@@ -361,6 +361,16 @@ EOFCONFIG
|
|||||||
wireplumberReloading = true;
|
wireplumberReloading = true;
|
||||||
wireplumberReloadStarted();
|
wireplumberReloadStarted();
|
||||||
|
|
||||||
|
if (!SessionService.systemctlCommandAvailable) {
|
||||||
|
Proc.runCommand("restartWireplumber", ["sh", "-c", "pkill -x wireplumber; sleep 1"], () => {
|
||||||
|
Quickshell.execDetached(["wireplumber"]);
|
||||||
|
wireplumberReloading = false;
|
||||||
|
ToastService.showInfo(I18n.tr("Audio system restarted"), I18n.tr("Device names updated"));
|
||||||
|
wireplumberReloadCompleted(true);
|
||||||
|
}, 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Proc.runCommand("restartWireplumber", ["systemctl", "--user", "restart", "wireplumber"], (output, exitCode) => {
|
Proc.runCommand("restartWireplumber", ["systemctl", "--user", "restart", "wireplumber"], (output, exitCode) => {
|
||||||
wireplumberReloading = false;
|
wireplumberReloading = false;
|
||||||
|
|
||||||
|
|||||||
@@ -948,8 +948,12 @@ Singleton {
|
|||||||
// systemd user environment from previous sessions and lie. Unset
|
// systemd user environment from previous sessions and lie. Unset
|
||||||
// WAYLAND_DISPLAY falls back to "wayland-0", mirroring wl_display_connect.
|
// WAYLAND_DISPLAY falls back to "wayland-0", mirroring wl_display_connect.
|
||||||
// /proc/net/unix: field 6 is state (01 = listening), 7 inode, 8 bound path.
|
// /proc/net/unix: field 6 is state (01 = listening), 7 inode, 8 bound path.
|
||||||
|
// The BSDs have no /proc/net; sockstat(1) -l -u lists listening unix
|
||||||
|
// sockets as USER COMMAND PID FD PROTO LOCAL-ADDRESS.
|
||||||
function detectCompositor() {
|
function detectCompositor() {
|
||||||
const script = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$sock" ;; esac; inode=$(awk -v p="$sock" \'$8 == p && $6 == 1 {print $7; exit}\' /proc/net/unix); [ -n "$inode" ] || exit 1; fd=$(find /proc/[0-9]*/fd/ -mindepth 1 -maxdepth 1 -lname "socket:\\[$inode\\]" 2>/dev/null | head -n1); [ -n "$fd" ] || exit 1; pid="${fd#/proc/}"; cat "/proc/${pid%%/*}/comm"';
|
const procScript = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$sock" ;; esac; inode=$(awk -v p="$sock" \'$8 == p && $6 == 1 {print $7; exit}\' /proc/net/unix); [ -n "$inode" ] || exit 1; fd=$(find /proc/[0-9]*/fd/ -mindepth 1 -maxdepth 1 -lname "socket:\\[$inode\\]" 2>/dev/null | head -n1); [ -n "$fd" ] || exit 1; pid="${fd#/proc/}"; cat "/proc/${pid%%/*}/comm"';
|
||||||
|
const sockstatScript = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/var/run/user/$(id -u)}/$sock" ;; esac; sockstat -l -u | awk -v p="$sock" \'$6 == p {print $2; exit}\'';
|
||||||
|
const script = Qt.platform.os === "unix" ? sockstatScript : procScript;
|
||||||
Proc.runCommand("waylandSocketOwner", ["sh", "-c", script], (output, exitCode) => {
|
Proc.runCommand("waylandSocketOwner", ["sh", "-c", script], (output, exitCode) => {
|
||||||
const comm = (exitCode === 0 && output) ? output.trim().toLowerCase() : "";
|
const comm = (exitCode === 0 && output) ? output.trim().toLowerCase() : "";
|
||||||
const name = _compositorNameFromComm(comm);
|
const name = _compositorNameFromComm(comm);
|
||||||
@@ -1037,7 +1041,7 @@ Singleton {
|
|||||||
{
|
{
|
||||||
name: "labwc",
|
name: "labwc",
|
||||||
present: !!labwcPid,
|
present: !!labwcPid,
|
||||||
test: ["sh", "-c", "[ \"$(cat /proc/\"$LABWC_PID\"/comm 2>/dev/null)\" = labwc ]"],
|
test: ["sh", "-c", "[ \"$(ps -p \"$LABWC_PID\" -o comm= 2>/dev/null)\" = labwc ]"],
|
||||||
detail: "LABWC_PID " + labwcPid
|
detail: "LABWC_PID " + labwcPid
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("SessionService")
|
readonly property var log: Log.scoped("SessionService")
|
||||||
|
|
||||||
|
// Qt.platform.os is "unix" on the BSDs (qtdeclarative qqmlplatform.cpp)
|
||||||
|
readonly property bool isBSD: Qt.platform.os === "unix"
|
||||||
property bool hasUwsm: false
|
property bool hasUwsm: false
|
||||||
property bool isElogind: false
|
property bool isElogind: false
|
||||||
property bool loginctlCommandAvailable: false
|
property bool loginctlCommandAvailable: false
|
||||||
@@ -351,10 +353,26 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function powerManagerCommand(action) {
|
function powerManagerCommand(action) {
|
||||||
|
if (isBSD)
|
||||||
|
return bsdPowerCommand(action);
|
||||||
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
|
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
|
||||||
return [useLoginctl ? "loginctl" : "systemctl", action];
|
return [useLoginctl ? "loginctl" : "systemctl", action];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bsdPowerCommand(action) {
|
||||||
|
switch (action) {
|
||||||
|
case "suspend":
|
||||||
|
case "suspend-then-hibernate":
|
||||||
|
return ["acpiconf", "-s", "3"];
|
||||||
|
case "hibernate":
|
||||||
|
return ["acpiconf", "-s", "4"];
|
||||||
|
case "reboot":
|
||||||
|
return ["shutdown", "-r", "now"];
|
||||||
|
default:
|
||||||
|
return ["shutdown", "-p", "now"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function suspend() {
|
function suspend() {
|
||||||
if (SettingsData.customPowerActionSuspend.length === 0) {
|
if (SettingsData.customPowerActionSuspend.length === 0) {
|
||||||
Quickshell.execDetached(powerManagerCommand("suspend"));
|
Quickshell.execDetached(powerManagerCommand("suspend"));
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ Singleton {
|
|||||||
|
|
||||||
readonly property var log: Log.scoped("UsersService")
|
readonly property var log: Log.scoped("UsersService")
|
||||||
|
|
||||||
|
// Qt.platform.os is "unix" on the BSDs; user management goes through pw(8)
|
||||||
|
readonly property bool isBSD: Qt.platform.os === "unix"
|
||||||
|
|
||||||
property var users: []
|
property var users: []
|
||||||
property string adminGroup: "wheel"
|
property string adminGroup: "wheel"
|
||||||
property string greeterGroup: "greeter"
|
property string greeterGroup: "greeter"
|
||||||
@@ -286,7 +289,7 @@ Singleton {
|
|||||||
property string op: "passwd"
|
property string op: "passwd"
|
||||||
property var cb: null
|
property var cb: null
|
||||||
property string capturedErr: ""
|
property string capturedErr: ""
|
||||||
command: ["pkexec", "sh", "-c", "head -n1 | chpasswd"]
|
command: root.isBSD ? ["pkexec", "pw", "usermod", "-n", targetUser, "-h", "0"] : ["pkexec", "sh", "-c", "head -n1 | chpasswd"]
|
||||||
stdinEnabled: true
|
stdinEnabled: true
|
||||||
running: false
|
running: false
|
||||||
stdout: StdioCollector {}
|
stdout: StdioCollector {}
|
||||||
@@ -294,7 +297,10 @@ Singleton {
|
|||||||
onStreamFinished: chpasswdProc.capturedErr = text || ""
|
onStreamFinished: chpasswdProc.capturedErr = text || ""
|
||||||
}
|
}
|
||||||
onStarted: {
|
onStarted: {
|
||||||
chpasswdProc.write(chpasswdProc.targetUser + ":" + chpasswdProc.targetPassword + "\n");
|
if (root.isBSD)
|
||||||
|
chpasswdProc.write(chpasswdProc.targetPassword + "\n");
|
||||||
|
else
|
||||||
|
chpasswdProc.write(chpasswdProc.targetUser + ":" + chpasswdProc.targetPassword + "\n");
|
||||||
}
|
}
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
const op = chpasswdProc.op;
|
const op = chpasswdProc.op;
|
||||||
@@ -414,7 +420,7 @@ Singleton {
|
|||||||
|
|
||||||
function _runUseradd(username, password, addToAdmin, addToGreeter, callback) {
|
function _runUseradd(username, password, addToAdmin, addToGreeter, callback) {
|
||||||
const proc = useraddComp.createObject(root, {
|
const proc = useraddComp.createObject(root, {
|
||||||
command: ["pkexec", "useradd", "-m", "-s", "/bin/bash", username],
|
command: root.isBSD ? ["pkexec", "pw", "useradd", "-n", username, "-m", "-s", "/bin/sh"] : ["pkexec", "useradd", "-m", "-s", "/bin/bash", username],
|
||||||
targetUser: username,
|
targetUser: username,
|
||||||
targetPassword: password,
|
targetPassword: password,
|
||||||
addAdmin: addToAdmin,
|
addAdmin: addToAdmin,
|
||||||
@@ -436,15 +442,21 @@ Singleton {
|
|||||||
|
|
||||||
function _runUserdel(username, callback) {
|
function _runUserdel(username, callback) {
|
||||||
const proc = userdelComp.createObject(root, {
|
const proc = userdelComp.createObject(root, {
|
||||||
command: ["pkexec", "userdel", "-r", username],
|
command: root.isBSD ? ["pkexec", "pw", "userdel", "-n", username, "-r"] : ["pkexec", "userdel", "-r", username],
|
||||||
targetUser: username,
|
targetUser: username,
|
||||||
cb: callback
|
cb: callback
|
||||||
});
|
});
|
||||||
proc.running = true;
|
proc.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _groupMemberCmd(group, username, add) {
|
||||||
|
if (root.isBSD)
|
||||||
|
return ["pkexec", "pw", "groupmod", group, add ? "-m" : "-d", username];
|
||||||
|
return add ? ["pkexec", "usermod", "-aG", group, username] : ["pkexec", "gpasswd", "-d", username, group];
|
||||||
|
}
|
||||||
|
|
||||||
function _runAdminToggle(username, makeAdmin, callback) {
|
function _runAdminToggle(username, makeAdmin, callback) {
|
||||||
const cmd = makeAdmin ? ["pkexec", "usermod", "-aG", root.adminGroup, username] : ["pkexec", "gpasswd", "-d", username, root.adminGroup];
|
const cmd = _groupMemberCmd(root.adminGroup, username, makeAdmin);
|
||||||
const proc = adminToggleComp.createObject(root, {
|
const proc = adminToggleComp.createObject(root, {
|
||||||
command: cmd,
|
command: cmd,
|
||||||
targetUser: username,
|
targetUser: username,
|
||||||
@@ -455,7 +467,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _runGreeterToggle(username, enableGreeter, callback) {
|
function _runGreeterToggle(username, enableGreeter, callback) {
|
||||||
const cmd = enableGreeter ? ["pkexec", "usermod", "-aG", root.greeterGroup, username] : ["pkexec", "gpasswd", "-d", username, root.greeterGroup];
|
const cmd = _groupMemberCmd(root.greeterGroup, username, enableGreeter);
|
||||||
const proc = greeterToggleComp.createObject(root, {
|
const proc = greeterToggleComp.createObject(root, {
|
||||||
command: cmd,
|
command: cmd,
|
||||||
targetUser: username,
|
targetUser: username,
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ Singleton {
|
|||||||
property int _geocodeReqId: 0
|
property int _geocodeReqId: 0
|
||||||
property var _pendingCoords: null
|
property var _pendingCoords: null
|
||||||
|
|
||||||
readonly property var lowPriorityCmd: ["nice", "-n", "19", "ionice", "-c3"]
|
// ionice is util-linux only; the BSDs get plain nice
|
||||||
|
readonly property var lowPriorityCmd: Qt.platform.os === "linux" ? ["nice", "-n", "19", "ionice", "-c3"] : ["nice", "-n", "19"]
|
||||||
readonly property var curlBaseCmd: ["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "100k", "--compressed"]
|
readonly property var curlBaseCmd: ["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "100k", "--compressed"]
|
||||||
|
|
||||||
property var weatherIcons: ({
|
property var weatherIcons: ({
|
||||||
|
|||||||
Reference in New Issue
Block a user