cef: harden shutdown and default VAAPI Vulkan stack
- commit
- 7f761e37e880f6e1677977d64b6f77a0b4416f6e
- author
- brice · · 2026-05-01 10:51 UTC
- parent
- c460950
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
16 files · +330 -33
M Makefile +5 -5
| @@ -42,7 +42,7 @@ help: ## Show this help message | |||
| 42 | 42 | build: build-systemviews ## Build the application (pure Go, no CGO) | |
| 43 | 43 | @echo "Building $(BINARY_NAME) $(VERSION) using $(NPROCS) cores..." | |
| 44 | 44 | @mkdir -p $(DIST_DIR) | |
| 45 | - | CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | |
| 45 | + | GOFLAGS=-mod=mod CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | |
| 46 | 46 | @rm -f $(DIST_DIR)/cef-helper | |
| 47 | 47 | @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | |
| 48 | 48 | ||
| @@ -63,7 +63,7 @@ build-systemviews: generate-systemviews ## Build the WASM systemviews runtime | |||
| 63 | 63 | build-quick: ## Build quickly for backend development | |
| 64 | 64 | @echo "Building $(BINARY_NAME) $(VERSION) (quick)..." | |
| 65 | 65 | @mkdir -p $(DIST_DIR) | |
| 66 | - | CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | |
| 66 | + | GOFLAGS=-mod=mod CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | |
| 67 | 67 | @rm -f $(DIST_DIR)/cef-helper | |
| 68 | 68 | @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | |
| 69 | 69 | ||
| @@ -102,15 +102,15 @@ mocks: ## Generate mock implementations with mockery | |||
| 102 | 102 | # Testing | |
| 103 | 103 | test: ## Run tests | |
| 104 | 104 | @echo "Running tests..." | |
| 105 | - | go test -v ./... | |
| 105 | + | GOFLAGS=-mod=mod go test -v ./... | |
| 106 | 106 | ||
| 107 | 107 | test-race: ## Run tests with race detection | |
| 108 | 108 | @echo "Running tests with race detection..." | |
| 109 | - | go test -race -v ./... | |
| 109 | + | GOFLAGS=-mod=mod go test -race -v ./... | |
| 110 | 110 | ||
| 111 | 111 | test-cover: ## Run tests with coverage | |
| 112 | 112 | @echo "Running tests with coverage..." | |
| 113 | - | go test -coverprofile=coverage.out ./... | |
| 113 | + | GOFLAGS=-mod=mod go test -coverprofile=coverage.out ./... | |
| 114 | 114 | go tool cover -html=coverage.out -o coverage.html | |
| 115 | 115 | @echo "Coverage report: coverage.html" | |
| 116 | 116 | ||
| @@ -42,7 +42,7 @@ help: ## Show this help message | |||||
| 42 | build: build-systemviews ## Build the application (pure Go, no CGO) | 42 | build: build-systemviews ## Build the application (pure Go, no CGO) | ||
| 43 | @echo "Building $(BINARY_NAME) $(VERSION) using $(NPROCS) cores..." | 43 | @echo "Building $(BINARY_NAME) $(VERSION) using $(NPROCS) cores..." | ||
| 44 | @mkdir -p $(DIST_DIR) | 44 | @mkdir -p $(DIST_DIR) | ||
| 45 | - | CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | 45 | + | GOFLAGS=-mod=mod CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) |
| 46 | @rm -f $(DIST_DIR)/cef-helper | 46 | @rm -f $(DIST_DIR)/cef-helper | ||
| 47 | @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | 47 | @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | ||
| 48 | 48 | ||||
| @@ -63,7 +63,7 @@ build-systemviews: generate-systemviews ## Build the WASM systemviews runtime | |||||
| 63 | build-quick: ## Build quickly for backend development | 63 | build-quick: ## Build quickly for backend development | ||
| 64 | @echo "Building $(BINARY_NAME) $(VERSION) (quick)..." | 64 | @echo "Building $(BINARY_NAME) $(VERSION) (quick)..." | ||
| 65 | @mkdir -p $(DIST_DIR) | 65 | @mkdir -p $(DIST_DIR) | ||
| 66 | - | CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) | 66 | + | GOFLAGS=-mod=mod CGO_ENABLED=0 go build -buildvcs=false -p $(NPROCS) $(GCFLAGS) $(LDFLAGS) -o $(DIST_DIR)/$(BINARY_NAME) $(MAIN_PATH) |
| 67 | @rm -f $(DIST_DIR)/cef-helper | 67 | @rm -f $(DIST_DIR)/cef-helper | ||
| 68 | @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | 68 | @echo "Build successful! Binary: $(DIST_DIR)/$(BINARY_NAME)" | ||
| 69 | 69 | ||||
| @@ -102,15 +102,15 @@ mocks: ## Generate mock implementations with mockery | |||||
| 102 | # Testing | 102 | # Testing | ||
| 103 | test: ## Run tests | 103 | test: ## Run tests | ||
| 104 | @echo "Running tests..." | 104 | @echo "Running tests..." | ||
| 105 | - | go test -v ./... | 105 | + | GOFLAGS=-mod=mod go test -v ./... |
| 106 | 106 | ||||
| 107 | test-race: ## Run tests with race detection | 107 | test-race: ## Run tests with race detection | ||
| 108 | @echo "Running tests with race detection..." | 108 | @echo "Running tests with race detection..." | ||
| 109 | - | go test -race -v ./... | 109 | + | GOFLAGS=-mod=mod go test -race -v ./... |
| 110 | 110 | ||||
| 111 | test-cover: ## Run tests with coverage | 111 | test-cover: ## Run tests with coverage | ||
| 112 | @echo "Running tests with coverage..." | 112 | @echo "Running tests with coverage..." | ||
| 113 | - | go test -coverprofile=coverage.out ./... | 113 | + | GOFLAGS=-mod=mod go test -coverprofile=coverage.out ./... |
| 114 | go tool cover -html=coverage.out -o coverage.html | 114 | go tool cover -html=coverage.out -o coverage.html | ||
| 115 | @echo "Coverage report: coverage.html" | 115 | @echo "Coverage report: coverage.html" | ||
| 116 | 116 | ||||
M cmd/dumber/main.go +1 -0
| @@ -327,6 +327,7 @@ func applyCEFRenderStackDefault(ctx context.Context, cfg *config.Config) { | |||
| 327 | 327 | return | |
| 328 | 328 | } | |
| 329 | 329 | infracef.ApplyDefaultRenderStackEnvironment(ctx) | |
| 330 | + | infracef.ApplyDefaultHardwareDecodeEnvironment(ctx, cfg) | |
| 330 | 331 | } | |
| 331 | 332 | ||
| 332 | 333 | func preInitializeAdwaitaForCEF(cfg *config.Config, initResult *bootstrap.ParallelInitResult, initAdwaita func()) { | |
| @@ -327,6 +327,7 @@ func applyCEFRenderStackDefault(ctx context.Context, cfg *config.Config) { | |||||
| 327 | return | 327 | return | ||
| 328 | } | 328 | } | ||
| 329 | infracef.ApplyDefaultRenderStackEnvironment(ctx) | 329 | infracef.ApplyDefaultRenderStackEnvironment(ctx) | ||
| 330 | + | infracef.ApplyDefaultHardwareDecodeEnvironment(ctx, cfg) | |||
| 330 | } | 331 | } | ||
| 331 | 332 | ||||
| 332 | func preInitializeAdwaitaForCEF(cfg *config.Config, initResult *bootstrap.ParallelInitResult, initAdwaita func()) { | 333 | func preInitializeAdwaitaForCEF(cfg *config.Config, initResult *bootstrap.ParallelInitResult, initAdwaita func()) { | ||
M go.sum +0 -2 generated file: collapsed
M internal/infrastructure/cef/app.go +3 -1
| @@ -70,12 +70,14 @@ func configureHardwareVideoDecode(commandLine purecef.CommandLine) { | |||
| 70 | 70 | ||
| 71 | 71 | appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumEnableFeaturesSwitch, | |
| 72 | 72 | "AcceleratedVideoDecoder", | |
| 73 | + | "AcceleratedVideoEncoder", | |
| 73 | 74 | "AcceleratedVideoDecodeLinuxGL", | |
| 74 | 75 | "AcceleratedVideoDecodeLinuxZeroCopyGL", | |
| 75 | 76 | "VaapiIgnoreDriverChecks", | |
| 76 | 77 | ) | |
| 77 | 78 | appendSwitchIfMissing(commandLine, "ignore-gpu-blocklist") | |
| 78 | 79 | appendSwitchIfMissing(commandLine, "enable-zero-copy") | |
| 80 | + | appendSwitchIfMissing(commandLine, "disable-gpu-driver-bug-workaround") | |
| 79 | 81 | } | |
| 80 | 82 | ||
| 81 | 83 | func configureEnvChromiumFlags(commandLine purecef.CommandLine) { | |
| @@ -336,7 +338,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine) | |||
| 336 | 338 | useAngle := "" | |
| 337 | 339 | ozonePlatform := "" | |
| 338 | 340 | if commandLine != nil { | |
| 339 | - | configureWebAuthnFeaturePolicy(commandLine) | |
| 341 | + | configureCommandLine(commandLine) | |
| 340 | 342 | appendSwitchIfMissing(commandLine, "no-zygote") | |
| 341 | 343 | processType = commandLine.GetSwitchValue("type") | |
| 342 | 344 | commandLineString = commandLine.GetCommandLineString() | |
| @@ -70,12 +70,14 @@ func configureHardwareVideoDecode(commandLine purecef.CommandLine) { | |||||
| 70 | 70 | ||||
| 71 | appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumEnableFeaturesSwitch, | 71 | appendUniqueCommaSeparatedSwitchValues(commandLine, chromiumEnableFeaturesSwitch, | ||
| 72 | "AcceleratedVideoDecoder", | 72 | "AcceleratedVideoDecoder", | ||
| 73 | + | "AcceleratedVideoEncoder", | |||
| 73 | "AcceleratedVideoDecodeLinuxGL", | 74 | "AcceleratedVideoDecodeLinuxGL", | ||
| 74 | "AcceleratedVideoDecodeLinuxZeroCopyGL", | 75 | "AcceleratedVideoDecodeLinuxZeroCopyGL", | ||
| 75 | "VaapiIgnoreDriverChecks", | 76 | "VaapiIgnoreDriverChecks", | ||
| 76 | ) | 77 | ) | ||
| 77 | appendSwitchIfMissing(commandLine, "ignore-gpu-blocklist") | 78 | appendSwitchIfMissing(commandLine, "ignore-gpu-blocklist") | ||
| 78 | appendSwitchIfMissing(commandLine, "enable-zero-copy") | 79 | appendSwitchIfMissing(commandLine, "enable-zero-copy") | ||
| 80 | + | appendSwitchIfMissing(commandLine, "disable-gpu-driver-bug-workaround") | |||
| 79 | } | 81 | } | ||
| 80 | 82 | ||||
| 81 | func configureEnvChromiumFlags(commandLine purecef.CommandLine) { | 83 | func configureEnvChromiumFlags(commandLine purecef.CommandLine) { | ||
| @@ -336,7 +338,7 @@ func (h *dumberBPH) OnBeforeChildProcessLaunch(commandLine purecef.CommandLine) | |||||
| 336 | useAngle := "" | 338 | useAngle := "" | ||
| 337 | ozonePlatform := "" | 339 | ozonePlatform := "" | ||
| 338 | if commandLine != nil { | 340 | if commandLine != nil { | ||
| 339 | - | configureWebAuthnFeaturePolicy(commandLine) | 341 | + | configureCommandLine(commandLine) |
| 340 | appendSwitchIfMissing(commandLine, "no-zygote") | 342 | appendSwitchIfMissing(commandLine, "no-zygote") | ||
| 341 | processType = commandLine.GetSwitchValue("type") | 343 | processType = commandLine.GetSwitchValue("type") | ||
| 342 | commandLineString = commandLine.GetCommandLineString() | 344 | commandLineString = commandLine.GetCommandLineString() | ||
M internal/infrastructure/cef/app_test.go +42 -2
| @@ -305,8 +305,18 @@ func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testin | |||
| 305 | 305 | ||
| 306 | 306 | configureCommandLine(commandLine) | |
| 307 | 307 | ||
| 308 | - | if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,AcceleratedVideoDecoder,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL,VaapiIgnoreDriverChecks" { | |
| 309 | - | t.Fatalf("enable-features = %q, want VAAPI features appended", got) | |
| 308 | + | gotFeatures := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch) | |
| 309 | + | for _, feature := range []string{ | |
| 310 | + | "ExistingFeature", | |
| 311 | + | "AcceleratedVideoDecoder", | |
| 312 | + | "AcceleratedVideoEncoder", | |
| 313 | + | "AcceleratedVideoDecodeLinuxGL", | |
| 314 | + | "AcceleratedVideoDecodeLinuxZeroCopyGL", | |
| 315 | + | "VaapiIgnoreDriverChecks", | |
| 316 | + | } { | |
| 317 | + | if !strings.Contains(gotFeatures, feature) { | |
| 318 | + | t.Fatalf("enable-features = %q, missing %q", gotFeatures, feature) | |
| 319 | + | } | |
| 310 | 320 | } | |
| 311 | 321 | if !commandLine.HasSwitch("ignore-gpu-blocklist") { | |
| 312 | 322 | t.Fatal("expected ignore-gpu-blocklist switch") | |
| @@ -314,6 +324,9 @@ func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testin | |||
| 314 | 324 | if !commandLine.HasSwitch("enable-zero-copy") { | |
| 315 | 325 | t.Fatal("expected enable-zero-copy switch") | |
| 316 | 326 | } | |
| 327 | + | if !commandLine.HasSwitch("disable-gpu-driver-bug-workaround") { | |
| 328 | + | t.Fatal("expected disable-gpu-driver-bug-workaround switch") | |
| 329 | + | } | |
| 317 | 330 | } | |
| 318 | 331 | ||
| 319 | 332 | func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) { | |
| @@ -420,6 +433,33 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) { | |||
| 420 | 433 | } | |
| 421 | 434 | } | |
| 422 | 435 | ||
| 436 | + | func TestDumberBPH_OnBeforeChildProcessLaunch_AppliesRenderingAndHardwareDecodeFlags(t *testing.T) { | |
| 437 | + | t.Setenv(cef2gtkAngleBackendVar, "vulkan") | |
| 438 | + | t.Setenv(cefEnableVAAPIEnvVar, "1") | |
| 439 | + | ||
| 440 | + | commandLine := newMutableCommandLineStub() | |
| 441 | + | commandLine.AppendSwitchWithValue("type", "gpu-process") | |
| 442 | + | commandLine.AppendSwitchWithValue("use-angle", "gl-egl") | |
| 443 | + | ||
| 444 | + | (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine) | |
| 445 | + | ||
| 446 | + | if got := commandLine.GetSwitchValue("use-angle"); got != "vulkan" { | |
| 447 | + | t.Fatalf("use-angle = %q, want vulkan", got) | |
| 448 | + | } | |
| 449 | + | if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); !strings.Contains(got, "AcceleratedVideoDecoder") || !strings.Contains(got, "VulkanFromANGLE") { | |
| 450 | + | t.Fatalf("enable-features = %q, want hardware decode and Vulkan ANGLE features", got) | |
| 451 | + | } | |
| 452 | + | if !commandLine.HasSwitch("ignore-gpu-blocklist") { | |
| 453 | + | t.Fatal("expected ignore-gpu-blocklist switch") | |
| 454 | + | } | |
| 455 | + | if !commandLine.HasSwitch("enable-zero-copy") { | |
| 456 | + | t.Fatal("expected enable-zero-copy switch") | |
| 457 | + | } | |
| 458 | + | if !commandLine.HasSwitch("no-zygote") { | |
| 459 | + | t.Fatal("expected no-zygote switch") | |
| 460 | + | } | |
| 461 | + | } | |
| 462 | + | ||
| 423 | 463 | func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) { | |
| 424 | 464 | t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "") | |
| 425 | 465 | ||
| @@ -305,8 +305,18 @@ func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testin | |||||
| 305 | 305 | ||||
| 306 | configureCommandLine(commandLine) | 306 | configureCommandLine(commandLine) | ||
| 307 | 307 | ||||
| 308 | - | if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); got != "ExistingFeature,AcceleratedVideoDecoder,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL,VaapiIgnoreDriverChecks" { | 308 | + | gotFeatures := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch) |
| 309 | - | t.Fatalf("enable-features = %q, want VAAPI features appended", got) | 309 | + | for _, feature := range []string{ |
| 310 | + | "ExistingFeature", | |||
| 311 | + | "AcceleratedVideoDecoder", | |||
| 312 | + | "AcceleratedVideoEncoder", | |||
| 313 | + | "AcceleratedVideoDecodeLinuxGL", | |||
| 314 | + | "AcceleratedVideoDecodeLinuxZeroCopyGL", | |||
| 315 | + | "VaapiIgnoreDriverChecks", | |||
| 316 | + | } { | |||
| 317 | + | if !strings.Contains(gotFeatures, feature) { | |||
| 318 | + | t.Fatalf("enable-features = %q, missing %q", gotFeatures, feature) | |||
| 319 | + | } | |||
| 310 | } | 320 | } | ||
| 311 | if !commandLine.HasSwitch("ignore-gpu-blocklist") { | 321 | if !commandLine.HasSwitch("ignore-gpu-blocklist") { | ||
| 312 | t.Fatal("expected ignore-gpu-blocklist switch") | 322 | t.Fatal("expected ignore-gpu-blocklist switch") | ||
| @@ -314,6 +324,9 @@ func TestConfigureCommandLine_EnableVAAPIEnvAppendsHardwareDecodeFlags(t *testin | |||||
| 314 | if !commandLine.HasSwitch("enable-zero-copy") { | 324 | if !commandLine.HasSwitch("enable-zero-copy") { | ||
| 315 | t.Fatal("expected enable-zero-copy switch") | 325 | t.Fatal("expected enable-zero-copy switch") | ||
| 316 | } | 326 | } | ||
| 327 | + | if !commandLine.HasSwitch("disable-gpu-driver-bug-workaround") { | |||
| 328 | + | t.Fatal("expected disable-gpu-driver-bug-workaround switch") | |||
| 329 | + | } | |||
| 317 | } | 330 | } | ||
| 318 | 331 | ||||
| 319 | func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) { | 332 | func TestConfigureCommandLine_ChromiumFlagsEnvAppendsSwitches(t *testing.T) { | ||
| @@ -420,6 +433,33 @@ func TestDumberBPH_OnBeforeChildProcessLaunch_AppendsNoZygote(t *testing.T) { | |||||
| 420 | } | 433 | } | ||
| 421 | } | 434 | } | ||
| 422 | 435 | ||||
| 436 | + | func TestDumberBPH_OnBeforeChildProcessLaunch_AppliesRenderingAndHardwareDecodeFlags(t *testing.T) { | |||
| 437 | + | t.Setenv(cef2gtkAngleBackendVar, "vulkan") | |||
| 438 | + | t.Setenv(cefEnableVAAPIEnvVar, "1") | |||
| 439 | + | ||||
| 440 | + | commandLine := newMutableCommandLineStub() | |||
| 441 | + | commandLine.AppendSwitchWithValue("type", "gpu-process") | |||
| 442 | + | commandLine.AppendSwitchWithValue("use-angle", "gl-egl") | |||
| 443 | + | ||||
| 444 | + | (&dumberBPH{engine: &Engine{ctx: context.Background()}}).OnBeforeChildProcessLaunch(commandLine) | |||
| 445 | + | ||||
| 446 | + | if got := commandLine.GetSwitchValue("use-angle"); got != "vulkan" { | |||
| 447 | + | t.Fatalf("use-angle = %q, want vulkan", got) | |||
| 448 | + | } | |||
| 449 | + | if got := commandLine.GetSwitchValue(chromiumEnableFeaturesSwitch); !strings.Contains(got, "AcceleratedVideoDecoder") || !strings.Contains(got, "VulkanFromANGLE") { | |||
| 450 | + | t.Fatalf("enable-features = %q, want hardware decode and Vulkan ANGLE features", got) | |||
| 451 | + | } | |||
| 452 | + | if !commandLine.HasSwitch("ignore-gpu-blocklist") { | |||
| 453 | + | t.Fatal("expected ignore-gpu-blocklist switch") | |||
| 454 | + | } | |||
| 455 | + | if !commandLine.HasSwitch("enable-zero-copy") { | |||
| 456 | + | t.Fatal("expected enable-zero-copy switch") | |||
| 457 | + | } | |||
| 458 | + | if !commandLine.HasSwitch("no-zygote") { | |||
| 459 | + | t.Fatal("expected no-zygote switch") | |||
| 460 | + | } | |||
| 461 | + | } | |||
| 462 | + | ||||
| 423 | func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) { | 463 | func TestDumberBPH_OnBeforeChildProcessLaunch_DisablesWebAuthnByDefault(t *testing.T) { | ||
| 424 | t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "") | 464 | t.Setenv(cefEnableWebAuthnUnsafeEnvVar, "") | ||
| 425 | 465 | ||||
M internal/infrastructure/cef/cef2gtk_adapter.go +11 -2
| @@ -24,8 +24,9 @@ var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed") | |||
| 24 | 24 | // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input | |
| 25 | 25 | // controller implementation — those belong to purego-cef2gtk. | |
| 26 | 26 | type Cef2gtkAdapter struct { | |
| 27 | - | view *cef2gtk.View | |
| 28 | - | destroyed atomic.Bool | |
| 27 | + | view *cef2gtk.View | |
| 28 | + | destroyed atomic.Bool | |
| 29 | + | destroyCnt atomic.Uint64 | |
| 29 | 30 | } | |
| 30 | 31 | ||
| 31 | 32 | // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin | |
| @@ -196,6 +197,7 @@ func (a *Cef2gtkAdapter) Destroy() error { | |||
| 196 | 197 | } | |
| 197 | 198 | err := a.view.Destroy() | |
| 198 | 199 | a.view = nil | |
| 200 | + | a.destroyCnt.Add(1) | |
| 199 | 201 | return err | |
| 200 | 202 | } | |
| 201 | 203 | ||
| @@ -203,3 +205,10 @@ func (a *Cef2gtkAdapter) Destroy() error { | |||
| 203 | 205 | func (a *Cef2gtkAdapter) IsDestroyed() bool { | |
| 204 | 206 | return a == nil || a.destroyed.Load() | |
| 205 | 207 | } | |
| 208 | + | ||
| 209 | + | func (a *Cef2gtkAdapter) destroyCount() uint64 { | |
| 210 | + | if a == nil { | |
| 211 | + | return 0 | |
| 212 | + | } | |
| 213 | + | return a.destroyCnt.Load() | |
| 214 | + | } | |
| @@ -24,8 +24,9 @@ var ErrAdapterDestroyed = errors.New("cef2gtk_adapter: adapter is destroyed") | |||||
| 24 | // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input | 24 | // The adapter does NOT own GL/PBO staging, dirty-rect uploads, or GTK input | ||
| 25 | // controller implementation — those belong to purego-cef2gtk. | 25 | // controller implementation — those belong to purego-cef2gtk. | ||
| 26 | type Cef2gtkAdapter struct { | 26 | type Cef2gtkAdapter struct { | ||
| 27 | - | view *cef2gtk.View | 27 | + | view *cef2gtk.View |
| 28 | - | destroyed atomic.Bool | 28 | + | destroyed atomic.Bool |
| 29 | + | destroyCnt atomic.Uint64 | |||
| 29 | } | 30 | } | ||
| 30 | 31 | ||||
| 31 | // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin | 32 | // NewCef2gtkAdapter creates an accelerated CEF view and wraps it in a thin | ||
| @@ -196,6 +197,7 @@ func (a *Cef2gtkAdapter) Destroy() error { | |||||
| 196 | } | 197 | } | ||
| 197 | err := a.view.Destroy() | 198 | err := a.view.Destroy() | ||
| 198 | a.view = nil | 199 | a.view = nil | ||
| 200 | + | a.destroyCnt.Add(1) | |||
| 199 | return err | 201 | return err | ||
| 200 | } | 202 | } | ||
| 201 | 203 | ||||
| @@ -203,3 +205,10 @@ func (a *Cef2gtkAdapter) Destroy() error { | |||||
| 203 | func (a *Cef2gtkAdapter) IsDestroyed() bool { | 205 | func (a *Cef2gtkAdapter) IsDestroyed() bool { | ||
| 204 | return a == nil || a.destroyed.Load() | 206 | return a == nil || a.destroyed.Load() | ||
| 205 | } | 207 | } | ||
| 208 | + | ||||
| 209 | + | func (a *Cef2gtkAdapter) destroyCount() uint64 { | |||
| 210 | + | if a == nil { | |||
| 211 | + | return 0 | |||
| 212 | + | } | |||
| 213 | + | return a.destroyCnt.Load() | |||
| 214 | + | } | |||
M internal/infrastructure/cef/engine.go +23 -1
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "crypto/subtle" | |
| 6 | 6 | "fmt" | |
| 7 | + | "os" | |
| 7 | 8 | "sync" | |
| 8 | 9 | "sync/atomic" | |
| 9 | 10 | "time" | |
| @@ -27,6 +28,7 @@ type Engine struct { | |||
| 27 | 28 | factory *WebViewFactory | |
| 28 | 29 | pool *WebViewPool | |
| 29 | 30 | profileLogDir string | |
| 31 | + | runtimeCEFDir string | |
| 30 | 32 | ||
| 31 | 33 | messageRouter *MessageRouter | |
| 32 | 34 | schemeHandler *dumbSchemeHandler | |
| @@ -215,7 +217,7 @@ func (e *Engine) Close() error { | |||
| 215 | 217 | return nil | |
| 216 | 218 | } | |
| 217 | 219 | ||
| 218 | - | const cefShutdownWaitTimeout = 2 * time.Second | |
| 220 | + | const cefShutdownWaitTimeout = 10 * time.Second | |
| 219 | 221 | ||
| 220 | 222 | func (e *Engine) activeWebViewCount() int { | |
| 221 | 223 | return int(e.activeCount.Load()) | |
| @@ -252,6 +254,7 @@ func (e *Engine) closeActiveWebViews() { | |||
| 252 | 254 | // Check immediately in case all webviews closed synchronously. | |
| 253 | 255 | if e.activeWebViewCount() == 0 { | |
| 254 | 256 | log.Debug().Msg("cef: all active webviews closed before shutdown") | |
| 257 | + | e.destroyClosedWebViewBridges(webViews) | |
| 255 | 258 | return | |
| 256 | 259 | } | |
| 257 | 260 | ||
| @@ -265,6 +268,15 @@ func (e *Engine) closeActiveWebViews() { | |||
| 265 | 268 | Str("timeout", cefShutdownWaitTimeout.String()). | |
| 266 | 269 | Msg("cef: timed out waiting for OnBeforeClose before shutdown") | |
| 267 | 270 | } | |
| 271 | + | e.destroyClosedWebViewBridges(webViews) | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | func (e *Engine) destroyClosedWebViewBridges(webViews []*WebView) { | |
| 275 | + | for _, wv := range webViews { | |
| 276 | + | if wv != nil && wv.destroyed.Load() { | |
| 277 | + | wv.destroyViewBridgeOnGTKSync() | |
| 278 | + | } | |
| 279 | + | } | |
| 268 | 280 | } | |
| 269 | 281 | ||
| 270 | 282 | // RegisterHandlers registers all WebUI message bridge handlers with the message router. | |
| @@ -375,6 +387,16 @@ func (e *Engine) notifyClipboardCopied(text string) { | |||
| 375 | 387 | // SetHandlerContext sets the base context for message handler dispatch. | |
| 376 | 388 | func (e *Engine) SetHandlerContext(ctx context.Context) { | |
| 377 | 389 | e.ctx = ctx | |
| 390 | + | logger := logging.FromContext(ctx) | |
| 391 | + | logger.Info(). | |
| 392 | + | Str("settings_cef_dir", e.runtimeCEFDir). | |
| 393 | + | Str("env_cef_dir", os.Getenv("CEF_DIR")). | |
| 394 | + | Msg("cef: runtime selection") | |
| 395 | + | if libcefPath := loadedLibCEFPath(); libcefPath != "" { | |
| 396 | + | logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded") | |
| 397 | + | } else { | |
| 398 | + | logger.Warn().Msg("cef: runtime library loaded but libcef path was not found in /proc/self/maps") | |
| 399 | + | } | |
| 378 | 400 | if e.messageRouter != nil { | |
| 379 | 401 | e.messageRouter.SetBaseContext(ctx) | |
| 380 | 402 | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "crypto/subtle" | 5 | "crypto/subtle" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "os" | |||
| 7 | "sync" | 8 | "sync" | ||
| 8 | "sync/atomic" | 9 | "sync/atomic" | ||
| 9 | "time" | 10 | "time" | ||
| @@ -27,6 +28,7 @@ type Engine struct { | |||||
| 27 | factory *WebViewFactory | 28 | factory *WebViewFactory | ||
| 28 | pool *WebViewPool | 29 | pool *WebViewPool | ||
| 29 | profileLogDir string | 30 | profileLogDir string | ||
| 31 | + | runtimeCEFDir string | |||
| 30 | 32 | ||||
| 31 | messageRouter *MessageRouter | 33 | messageRouter *MessageRouter | ||
| 32 | schemeHandler *dumbSchemeHandler | 34 | schemeHandler *dumbSchemeHandler | ||
| @@ -215,7 +217,7 @@ func (e *Engine) Close() error { | |||||
| 215 | return nil | 217 | return nil | ||
| 216 | } | 218 | } | ||
| 217 | 219 | ||||
| 218 | - | const cefShutdownWaitTimeout = 2 * time.Second | 220 | + | const cefShutdownWaitTimeout = 10 * time.Second |
| 219 | 221 | ||||
| 220 | func (e *Engine) activeWebViewCount() int { | 222 | func (e *Engine) activeWebViewCount() int { | ||
| 221 | return int(e.activeCount.Load()) | 223 | return int(e.activeCount.Load()) | ||
| @@ -252,6 +254,7 @@ func (e *Engine) closeActiveWebViews() { | |||||
| 252 | // Check immediately in case all webviews closed synchronously. | 254 | // Check immediately in case all webviews closed synchronously. | ||
| 253 | if e.activeWebViewCount() == 0 { | 255 | if e.activeWebViewCount() == 0 { | ||
| 254 | log.Debug().Msg("cef: all active webviews closed before shutdown") | 256 | log.Debug().Msg("cef: all active webviews closed before shutdown") | ||
| 257 | + | e.destroyClosedWebViewBridges(webViews) | |||
| 255 | return | 258 | return | ||
| 256 | } | 259 | } | ||
| 257 | 260 | ||||
| @@ -265,6 +268,15 @@ func (e *Engine) closeActiveWebViews() { | |||||
| 265 | Str("timeout", cefShutdownWaitTimeout.String()). | 268 | Str("timeout", cefShutdownWaitTimeout.String()). | ||
| 266 | Msg("cef: timed out waiting for OnBeforeClose before shutdown") | 269 | Msg("cef: timed out waiting for OnBeforeClose before shutdown") | ||
| 267 | } | 270 | } | ||
| 271 | + | e.destroyClosedWebViewBridges(webViews) | |||
| 272 | + | } | |||
| 273 | + | ||||
| 274 | + | func (e *Engine) destroyClosedWebViewBridges(webViews []*WebView) { | |||
| 275 | + | for _, wv := range webViews { | |||
| 276 | + | if wv != nil && wv.destroyed.Load() { | |||
| 277 | + | wv.destroyViewBridgeOnGTKSync() | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 268 | } | 280 | } | ||
| 269 | 281 | ||||
| 270 | // RegisterHandlers registers all WebUI message bridge handlers with the message router. | 282 | // RegisterHandlers registers all WebUI message bridge handlers with the message router. | ||
| @@ -375,6 +387,16 @@ func (e *Engine) notifyClipboardCopied(text string) { | |||||
| 375 | // SetHandlerContext sets the base context for message handler dispatch. | 387 | // SetHandlerContext sets the base context for message handler dispatch. | ||
| 376 | func (e *Engine) SetHandlerContext(ctx context.Context) { | 388 | func (e *Engine) SetHandlerContext(ctx context.Context) { | ||
| 377 | e.ctx = ctx | 389 | e.ctx = ctx | ||
| 390 | + | logger := logging.FromContext(ctx) | |||
| 391 | + | logger.Info(). | |||
| 392 | + | Str("settings_cef_dir", e.runtimeCEFDir). | |||
| 393 | + | Str("env_cef_dir", os.Getenv("CEF_DIR")). | |||
| 394 | + | Msg("cef: runtime selection") | |||
| 395 | + | if libcefPath := loadedLibCEFPath(); libcefPath != "" { | |||
| 396 | + | logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded") | |||
| 397 | + | } else { | |||
| 398 | + | logger.Warn().Msg("cef: runtime library loaded but libcef path was not found in /proc/self/maps") | |||
| 399 | + | } | |||
| 378 | if e.messageRouter != nil { | 400 | if e.messageRouter != nil { | ||
| 379 | e.messageRouter.SetBaseContext(ctx) | 401 | e.messageRouter.SetBaseContext(ctx) | ||
| 380 | } | 402 | } | ||
M internal/infrastructure/cef/engine_init.go +32 -0
| @@ -58,6 +58,7 @@ func NewEngine( | |||
| 58 | 58 | eng := &Engine{ | |
| 59 | 59 | ctx: ctx, | |
| 60 | 60 | profileLogDir: paths.ProfileLogDir, | |
| 61 | + | runtimeCEFDir: settings.CEFDir, | |
| 61 | 62 | registerHandlers: deps.RegisterHandlers, | |
| 62 | 63 | registerAccentHandlers: deps.RegisterAccentHandlers, | |
| 63 | 64 | currentConfigPayload: deps.CurrentConfigPayload, | |
| @@ -153,14 +154,45 @@ func prepareCEFSettings( | |||
| 153 | 154 | // and browser-process callbacks (OnBeforeCommandLineProcessing, etc.). | |
| 154 | 155 | func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logger) error { | |
| 155 | 156 | app := newDumberApp(eng) | |
| 157 | + | logger.Info(). | |
| 158 | + | Str("settings_cef_dir", settings.CEFDir). | |
| 159 | + | Str("env_cef_dir", os.Getenv("CEF_DIR")). | |
| 160 | + | Msg("cef: runtime selection") | |
| 156 | 161 | logger.Debug().Msg("cef: calling InitWithApp") | |
| 157 | 162 | if err := purecef.InitWithApp(settings, app); err != nil { | |
| 158 | 163 | return fmt.Errorf("cef.InitWithApp: %w", err) | |
| 159 | 164 | } | |
| 165 | + | if libcefPath := loadedLibCEFPath(); libcefPath != "" { | |
| 166 | + | logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded") | |
| 167 | + | } else { | |
| 168 | + | logger.Warn().Msg("cef: runtime library loaded but libcef path was not found in /proc/self/maps") | |
| 169 | + | } | |
| 160 | 170 | logger.Debug().Msg("cef: InitWithApp returned OK") | |
| 161 | 171 | return nil | |
| 162 | 172 | } | |
| 163 | 173 | ||
| 174 | + | func loadedLibCEFPath() string { | |
| 175 | + | data, err := os.ReadFile("/proc/self/maps") | |
| 176 | + | if err != nil { | |
| 177 | + | return "" | |
| 178 | + | } | |
| 179 | + | return parseLoadedLibCEFPath(string(data)) | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | func parseLoadedLibCEFPath(maps string) string { | |
| 183 | + | for _, line := range strings.Split(maps, "\n") { | |
| 184 | + | if !strings.Contains(line, "libcef.so") { | |
| 185 | + | continue | |
| 186 | + | } | |
| 187 | + | pathStart := strings.Index(line, "/") | |
| 188 | + | if pathStart < 0 { | |
| 189 | + | continue | |
| 190 | + | } | |
| 191 | + | return strings.TrimSpace(line[pathStart:]) | |
| 192 | + | } | |
| 193 | + | return "" | |
| 194 | + | } | |
| 195 | + | ||
| 164 | 196 | // wireEngine creates factory, pool, and scheme handler after CEF init. | |
| 165 | 197 | func wireEngine( | |
| 166 | 198 | ctx context.Context, eng *Engine, | |
| @@ -58,6 +58,7 @@ func NewEngine( | |||||
| 58 | eng := &Engine{ | 58 | eng := &Engine{ | ||
| 59 | ctx: ctx, | 59 | ctx: ctx, | ||
| 60 | profileLogDir: paths.ProfileLogDir, | 60 | profileLogDir: paths.ProfileLogDir, | ||
| 61 | + | runtimeCEFDir: settings.CEFDir, | |||
| 61 | registerHandlers: deps.RegisterHandlers, | 62 | registerHandlers: deps.RegisterHandlers, | ||
| 62 | registerAccentHandlers: deps.RegisterAccentHandlers, | 63 | registerAccentHandlers: deps.RegisterAccentHandlers, | ||
| 63 | currentConfigPayload: deps.CurrentConfigPayload, | 64 | currentConfigPayload: deps.CurrentConfigPayload, | ||
| @@ -153,14 +154,45 @@ func prepareCEFSettings( | |||||
| 153 | // and browser-process callbacks (OnBeforeCommandLineProcessing, etc.). | 154 | // and browser-process callbacks (OnBeforeCommandLineProcessing, etc.). | ||
| 154 | func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logger) error { | 155 | func initializeCEF(eng *Engine, settings purecef.Settings, logger *zerolog.Logger) error { | ||
| 155 | app := newDumberApp(eng) | 156 | app := newDumberApp(eng) | ||
| 157 | + | logger.Info(). | |||
| 158 | + | Str("settings_cef_dir", settings.CEFDir). | |||
| 159 | + | Str("env_cef_dir", os.Getenv("CEF_DIR")). | |||
| 160 | + | Msg("cef: runtime selection") | |||
| 156 | logger.Debug().Msg("cef: calling InitWithApp") | 161 | logger.Debug().Msg("cef: calling InitWithApp") | ||
| 157 | if err := purecef.InitWithApp(settings, app); err != nil { | 162 | if err := purecef.InitWithApp(settings, app); err != nil { | ||
| 158 | return fmt.Errorf("cef.InitWithApp: %w", err) | 163 | return fmt.Errorf("cef.InitWithApp: %w", err) | ||
| 159 | } | 164 | } | ||
| 165 | + | if libcefPath := loadedLibCEFPath(); libcefPath != "" { | |||
| 166 | + | logger.Info().Str("libcef_path", libcefPath).Msg("cef: runtime library loaded") | |||
| 167 | + | } else { | |||
| 168 | + | logger.Warn().Msg("cef: runtime library loaded but libcef path was not found in /proc/self/maps") | |||
| 169 | + | } | |||
| 160 | logger.Debug().Msg("cef: InitWithApp returned OK") | 170 | logger.Debug().Msg("cef: InitWithApp returned OK") | ||
| 161 | return nil | 171 | return nil | ||
| 162 | } | 172 | } | ||
| 163 | 173 | ||||
| 174 | + | func loadedLibCEFPath() string { | |||
| 175 | + | data, err := os.ReadFile("/proc/self/maps") | |||
| 176 | + | if err != nil { | |||
| 177 | + | return "" | |||
| 178 | + | } | |||
| 179 | + | return parseLoadedLibCEFPath(string(data)) | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | func parseLoadedLibCEFPath(maps string) string { | |||
| 183 | + | for _, line := range strings.Split(maps, "\n") { | |||
| 184 | + | if !strings.Contains(line, "libcef.so") { | |||
| 185 | + | continue | |||
| 186 | + | } | |||
| 187 | + | pathStart := strings.Index(line, "/") | |||
| 188 | + | if pathStart < 0 { | |||
| 189 | + | continue | |||
| 190 | + | } | |||
| 191 | + | return strings.TrimSpace(line[pathStart:]) | |||
| 192 | + | } | |||
| 193 | + | return "" | |||
| 194 | + | } | |||
| 195 | + | ||||
| 164 | // wireEngine creates factory, pool, and scheme handler after CEF init. | 196 | // wireEngine creates factory, pool, and scheme handler after CEF init. | ||
| 165 | func wireEngine( | 197 | func wireEngine( | ||
| 166 | ctx context.Context, eng *Engine, | 198 | ctx context.Context, eng *Engine, | ||
M internal/infrastructure/cef/engine_init_test.go +12 -0
| @@ -46,6 +46,18 @@ func TestPrepareCEFSettings_UsesResolvedProfilePaths(t *testing.T) { | |||
| 46 | 46 | require.Empty(t, settings.BrowserSubprocessPath) | |
| 47 | 47 | } | |
| 48 | 48 | ||
| 49 | + | func TestParseLoadedLibCEFPath(t *testing.T) { | |
| 50 | + | maps := "7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n" + | |
| 51 | + | "7f5fe0000000-7f5fe8200000 r-xp 00000000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n" + | |
| 52 | + | "7f5fe8200000-7f5fe8400000 r--p 08200000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n" | |
| 53 | + | ||
| 54 | + | require.Equal(t, "/opt/cef-vaapi-runtime/usr/lib/cef/libcef.so", parseLoadedLibCEFPath(maps)) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func TestParseLoadedLibCEFPath_ReturnsEmptyWhenAbsent(t *testing.T) { | |
| 58 | + | require.Empty(t, parseLoadedLibCEFPath("7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n")) | |
| 59 | + | } | |
| 60 | + | ||
| 49 | 61 | func TestPrepareCEFSettings_RejectsNonDefaultCookiePolicy(t *testing.T) { | |
| 50 | 62 | logger := zerolog.Nop() | |
| 51 | 63 | profile := testCEFDevProfile(t) | |
| @@ -46,6 +46,18 @@ func TestPrepareCEFSettings_UsesResolvedProfilePaths(t *testing.T) { | |||||
| 46 | require.Empty(t, settings.BrowserSubprocessPath) | 46 | require.Empty(t, settings.BrowserSubprocessPath) | ||
| 47 | } | 47 | } | ||
| 48 | 48 | ||||
| 49 | + | func TestParseLoadedLibCEFPath(t *testing.T) { | |||
| 50 | + | maps := "7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n" + | |||
| 51 | + | "7f5fe0000000-7f5fe8200000 r-xp 00000000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n" + | |||
| 52 | + | "7f5fe8200000-7f5fe8400000 r--p 08200000 00:00 0 /opt/cef-vaapi-runtime/usr/lib/cef/libcef.so\n" | |||
| 53 | + | ||||
| 54 | + | require.Equal(t, "/opt/cef-vaapi-runtime/usr/lib/cef/libcef.so", parseLoadedLibCEFPath(maps)) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | func TestParseLoadedLibCEFPath_ReturnsEmptyWhenAbsent(t *testing.T) { | |||
| 58 | + | require.Empty(t, parseLoadedLibCEFPath("7f5fd4000000-7f5fd4200000 r--p 00000000 00:00 0 /usr/lib/libvulkan.so.1\n")) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 49 | func TestPrepareCEFSettings_RejectsNonDefaultCookiePolicy(t *testing.T) { | 61 | func TestPrepareCEFSettings_RejectsNonDefaultCookiePolicy(t *testing.T) { | ||
| 50 | logger := zerolog.Nop() | 62 | logger := zerolog.Nop() | ||
| 51 | profile := testCEFDevProfile(t) | 63 | profile := testCEFDevProfile(t) | ||
M internal/infrastructure/cef/render_stack_env.go +39 -2
| @@ -5,6 +5,9 @@ import ( | |||
| 5 | 5 | "os" | |
| 6 | 6 | "strings" | |
| 7 | 7 | ||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 10 | + | renderenv "github.com/bnema/dumber/internal/infrastructure/env" | |
| 8 | 11 | "github.com/bnema/dumber/internal/logging" | |
| 9 | 12 | ) | |
| 10 | 13 | ||
| @@ -30,7 +33,7 @@ const ( | |||
| 30 | 33 | // | |
| 31 | 34 | // DUMBER_RENDER_STACK accepts: | |
| 32 | 35 | // - auto or empty: use the GPU-first GDK DMABUF stack | |
| 33 | - | // - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE GL/EGL | |
| 36 | + | // - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE Vulkan | |
| 34 | 37 | // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics | |
| 35 | 38 | // | |
| 36 | 39 | // Low-level env vars remain diagnostic escape hatches: when explicitly set, | |
| @@ -49,11 +52,45 @@ func applyDefaultRenderStackEnvironment(ctx context.Context) string { | |||
| 49 | 52 | default: | |
| 50 | 53 | setEnvDefault(ctx, gskRendererEnvVar, gskRendererVulkan) | |
| 51 | 54 | setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF) | |
| 52 | - | setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleGLEGL) | |
| 55 | + | setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleVulkan) | |
| 53 | 56 | return renderStackVulkanDMABUF | |
| 54 | 57 | } | |
| 55 | 58 | } | |
| 56 | 59 | ||
| 60 | + | // ApplyDefaultHardwareDecodeEnvironment maps Dumber's media config to CEF's | |
| 61 | + | // developer-facing VAAPI switch env var before CEF command-line callbacks run. | |
| 62 | + | // Existing explicit env values are preserved as low-level escape hatches. | |
| 63 | + | func ApplyDefaultHardwareDecodeEnvironment(ctx context.Context, cfg *config.Config) { | |
| 64 | + | if cfg == nil || cfg.Engine.ResolveEngineType() != config.EngineTypeCEF { | |
| 65 | + | return | |
| 66 | + | } | |
| 67 | + | if cfg.Media.HardwareDecodingMode == config.HardwareDecodingDisable { | |
| 68 | + | setEnvDefault(ctx, cefEnableVAAPIEnvVar, "0") | |
| 69 | + | return | |
| 70 | + | } | |
| 71 | + | setEnvDefault(ctx, cefEnableVAAPIEnvVar, "1") | |
| 72 | + | applyDefaultLIBVADriverEnvironment(ctx) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func applyDefaultLIBVADriverEnvironment(ctx context.Context) { | |
| 76 | + | if strings.TrimSpace(os.Getenv("LIBVA_DRIVER_NAME")) != "" { | |
| 77 | + | return | |
| 78 | + | } | |
| 79 | + | manager := renderenv.NewManager() | |
| 80 | + | switch manager.DetectGPUVendor(ctx) { | |
| 81 | + | case port.GPUVendorAMD: | |
| 82 | + | setEnvDefault(ctx, "LIBVA_DRIVER_NAME", "radeonsi") | |
| 83 | + | case port.GPUVendorIntel: | |
| 84 | + | setEnvDefault(ctx, "LIBVA_DRIVER_NAME", "iHD") | |
| 85 | + | case port.GPUVendorNVIDIA: | |
| 86 | + | if ctx == nil { | |
| 87 | + | ctx = context.Background() | |
| 88 | + | } | |
| 89 | + | logging.FromContext(ctx).Warn(). | |
| 90 | + | Msg("cef: not defaulting LIBVA_DRIVER_NAME for NVIDIA; Chromium/CEF VAAPI support is driver-dependent") | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | ||
| 57 | 94 | func normalizeRenderStack(value string) string { | |
| 58 | 95 | switch strings.ToLower(strings.TrimSpace(value)) { | |
| 59 | 96 | case "", renderStackAuto, renderStackVulkanDMABUF: | |
| @@ -5,6 +5,9 @@ import ( | |||||
| 5 | "os" | 5 | "os" | ||
| 6 | "strings" | 6 | "strings" | ||
| 7 | 7 | ||||
| 8 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 9 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 10 | + | renderenv "github.com/bnema/dumber/internal/infrastructure/env" | |||
| 8 | "github.com/bnema/dumber/internal/logging" | 11 | "github.com/bnema/dumber/internal/logging" | ||
| 9 | ) | 12 | ) | ||
| 10 | 13 | ||||
| @@ -30,7 +33,7 @@ const ( | |||||
| 30 | // | 33 | // | ||
| 31 | // DUMBER_RENDER_STACK accepts: | 34 | // DUMBER_RENDER_STACK accepts: | ||
| 32 | // - auto or empty: use the GPU-first GDK DMABUF stack | 35 | // - auto or empty: use the GPU-first GDK DMABUF stack | ||
| 33 | - | // - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE GL/EGL | 36 | + | // - vulkan-dmabuf: force GDK DMABUF presentation with GSK Vulkan and CEF ANGLE Vulkan |
| 34 | // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics | 37 | // - legacy-gl: force the older GtkGLArea/OpenGL bridge for diagnostics | ||
| 35 | // | 38 | // | ||
| 36 | // Low-level env vars remain diagnostic escape hatches: when explicitly set, | 39 | // Low-level env vars remain diagnostic escape hatches: when explicitly set, | ||
| @@ -49,11 +52,45 @@ func applyDefaultRenderStackEnvironment(ctx context.Context) string { | |||||
| 49 | default: | 52 | default: | ||
| 50 | setEnvDefault(ctx, gskRendererEnvVar, gskRendererVulkan) | 53 | setEnvDefault(ctx, gskRendererEnvVar, gskRendererVulkan) | ||
| 51 | setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF) | 54 | setEnvDefault(ctx, cef2gtkBackendEnvVar, cef2gtkBackendGDKDMABUF) | ||
| 52 | - | setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleGLEGL) | 55 | + | setEnvDefault(ctx, cef2gtkAngleBackendVar, cef2gtkAngleVulkan) |
| 53 | return renderStackVulkanDMABUF | 56 | return renderStackVulkanDMABUF | ||
| 54 | } | 57 | } | ||
| 55 | } | 58 | } | ||
| 56 | 59 | ||||
| 60 | + | // ApplyDefaultHardwareDecodeEnvironment maps Dumber's media config to CEF's | |||
| 61 | + | // developer-facing VAAPI switch env var before CEF command-line callbacks run. | |||
| 62 | + | // Existing explicit env values are preserved as low-level escape hatches. | |||
| 63 | + | func ApplyDefaultHardwareDecodeEnvironment(ctx context.Context, cfg *config.Config) { | |||
| 64 | + | if cfg == nil || cfg.Engine.ResolveEngineType() != config.EngineTypeCEF { | |||
| 65 | + | return | |||
| 66 | + | } | |||
| 67 | + | if cfg.Media.HardwareDecodingMode == config.HardwareDecodingDisable { | |||
| 68 | + | setEnvDefault(ctx, cefEnableVAAPIEnvVar, "0") | |||
| 69 | + | return | |||
| 70 | + | } | |||
| 71 | + | setEnvDefault(ctx, cefEnableVAAPIEnvVar, "1") | |||
| 72 | + | applyDefaultLIBVADriverEnvironment(ctx) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func applyDefaultLIBVADriverEnvironment(ctx context.Context) { | |||
| 76 | + | if strings.TrimSpace(os.Getenv("LIBVA_DRIVER_NAME")) != "" { | |||
| 77 | + | return | |||
| 78 | + | } | |||
| 79 | + | manager := renderenv.NewManager() | |||
| 80 | + | switch manager.DetectGPUVendor(ctx) { | |||
| 81 | + | case port.GPUVendorAMD: | |||
| 82 | + | setEnvDefault(ctx, "LIBVA_DRIVER_NAME", "radeonsi") | |||
| 83 | + | case port.GPUVendorIntel: | |||
| 84 | + | setEnvDefault(ctx, "LIBVA_DRIVER_NAME", "iHD") | |||
| 85 | + | case port.GPUVendorNVIDIA: | |||
| 86 | + | if ctx == nil { | |||
| 87 | + | ctx = context.Background() | |||
| 88 | + | } | |||
| 89 | + | logging.FromContext(ctx).Warn(). | |||
| 90 | + | Msg("cef: not defaulting LIBVA_DRIVER_NAME for NVIDIA; Chromium/CEF VAAPI support is driver-dependent") | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | ||||
| 57 | func normalizeRenderStack(value string) string { | 94 | func normalizeRenderStack(value string) string { | ||
| 58 | switch strings.ToLower(strings.TrimSpace(value)) { | 95 | switch strings.ToLower(strings.TrimSpace(value)) { | ||
| 59 | case "", renderStackAuto, renderStackVulkanDMABUF: | 96 | case "", renderStackAuto, renderStackVulkanDMABUF: | ||
M internal/infrastructure/cef/render_stack_env_test.go +58 -3
| @@ -4,9 +4,11 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "os" | |
| 6 | 6 | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 7 | 9 | ) | |
| 8 | 10 | ||
| 9 | - | func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEGL(t *testing.T) { | |
| 11 | + | func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t *testing.T) { | |
| 10 | 12 | t.Setenv(dumberRenderStackEnvVar, "") | |
| 11 | 13 | t.Setenv("GSK_RENDERER", "") | |
| 12 | 14 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | |
| @@ -23,8 +25,8 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEGL(t *te | |||
| 23 | 25 | if got := os.Getenv("PUREGO_CEF2GTK_BACKEND"); got != "gdk-dmabuf" { | |
| 24 | 26 | t.Fatalf("PUREGO_CEF2GTK_BACKEND = %q, want gdk-dmabuf", got) | |
| 25 | 27 | } | |
| 26 | - | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" { | |
| 27 | - | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got) | |
| 28 | + | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" { | |
| 29 | + | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want vulkan", got) | |
| 28 | 30 | } | |
| 29 | 31 | } | |
| 30 | 32 | ||
| @@ -68,3 +70,56 @@ func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | |||
| 68 | 70 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got) | |
| 69 | 71 | } | |
| 70 | 72 | } | |
| 73 | + | ||
| 74 | + | func TestApplyDefaultHardwareDecodeEnvironment_DefaultsCEFToVAAPIForAuto(t *testing.T) { | |
| 75 | + | t.Setenv(cefEnableVAAPIEnvVar, "") | |
| 76 | + | ||
| 77 | + | ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{ | |
| 78 | + | Engine: config.EngineConfig{Type: config.EngineTypeCEF}, | |
| 79 | + | Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto}, | |
| 80 | + | }) | |
| 81 | + | ||
| 82 | + | if got := os.Getenv(cefEnableVAAPIEnvVar); got != "1" { | |
| 83 | + | t.Fatalf("%s = %q, want 1", cefEnableVAAPIEnvVar, got) | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func TestApplyDefaultHardwareDecodeEnvironment_PreservesExplicitLIBVADriver(t *testing.T) { | |
| 88 | + | t.Setenv(cefEnableVAAPIEnvVar, "") | |
| 89 | + | t.Setenv("LIBVA_DRIVER_NAME", "custom-driver") | |
| 90 | + | ||
| 91 | + | ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{ | |
| 92 | + | Engine: config.EngineConfig{Type: config.EngineTypeCEF}, | |
| 93 | + | Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto}, | |
| 94 | + | }) | |
| 95 | + | ||
| 96 | + | if got := os.Getenv("LIBVA_DRIVER_NAME"); got != "custom-driver" { | |
| 97 | + | t.Fatalf("LIBVA_DRIVER_NAME = %q, want explicit custom-driver", got) | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func TestApplyDefaultHardwareDecodeEnvironment_DisablesCEFVAAPIWhenMediaDisabled(t *testing.T) { | |
| 102 | + | t.Setenv(cefEnableVAAPIEnvVar, "") | |
| 103 | + | ||
| 104 | + | ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{ | |
| 105 | + | Engine: config.EngineConfig{Type: config.EngineTypeCEF}, | |
| 106 | + | Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingDisable}, | |
| 107 | + | }) | |
| 108 | + | ||
| 109 | + | if got := os.Getenv(cefEnableVAAPIEnvVar); got != "0" { | |
| 110 | + | t.Fatalf("%s = %q, want 0", cefEnableVAAPIEnvVar, got) | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | func TestApplyDefaultHardwareDecodeEnvironment_PreservesExplicitCEFVAAPIOverride(t *testing.T) { | |
| 115 | + | t.Setenv(cefEnableVAAPIEnvVar, "0") | |
| 116 | + | ||
| 117 | + | ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{ | |
| 118 | + | Engine: config.EngineConfig{Type: config.EngineTypeCEF}, | |
| 119 | + | Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto}, | |
| 120 | + | }) | |
| 121 | + | ||
| 122 | + | if got := os.Getenv(cefEnableVAAPIEnvVar); got != "0" { | |
| 123 | + | t.Fatalf("%s = %q, want explicit 0", cefEnableVAAPIEnvVar, got) | |
| 124 | + | } | |
| 125 | + | } | |
| @@ -4,9 +4,11 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | + | ||||
| 8 | + | "github.com/bnema/dumber/internal/infrastructure/config" | |||
| 7 | ) | 9 | ) | ||
| 8 | 10 | ||||
| 9 | - | func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEGL(t *testing.T) { | 11 | + | func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEVulkan(t *testing.T) { |
| 10 | t.Setenv(dumberRenderStackEnvVar, "") | 12 | t.Setenv(dumberRenderStackEnvVar, "") | ||
| 11 | t.Setenv("GSK_RENDERER", "") | 13 | t.Setenv("GSK_RENDERER", "") | ||
| 12 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | 14 | t.Setenv("PUREGO_CEF2GTK_BACKEND", "") | ||
| @@ -23,8 +25,8 @@ func TestApplyDefaultRenderStackEnvironment_DefaultsToGDKDMABUFWithANGLEGL(t *te | |||||
| 23 | if got := os.Getenv("PUREGO_CEF2GTK_BACKEND"); got != "gdk-dmabuf" { | 25 | if got := os.Getenv("PUREGO_CEF2GTK_BACKEND"); got != "gdk-dmabuf" { | ||
| 24 | t.Fatalf("PUREGO_CEF2GTK_BACKEND = %q, want gdk-dmabuf", got) | 26 | t.Fatalf("PUREGO_CEF2GTK_BACKEND = %q, want gdk-dmabuf", got) | ||
| 25 | } | 27 | } | ||
| 26 | - | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "gl-egl" { | 28 | + | if got := os.Getenv("PUREGO_CEF2GTK_ANGLE_BACKEND"); got != "vulkan" { |
| 27 | - | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got) | 29 | + | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want vulkan", got) |
| 28 | } | 30 | } | ||
| 29 | } | 31 | } | ||
| 30 | 32 | ||||
| @@ -68,3 +70,56 @@ func TestApplyDefaultRenderStackEnvironment_LegacyGLUsesGLArea(t *testing.T) { | |||||
| 68 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got) | 70 | t.Fatalf("PUREGO_CEF2GTK_ANGLE_BACKEND = %q, want gl-egl", got) | ||
| 69 | } | 71 | } | ||
| 70 | } | 72 | } | ||
| 73 | + | ||||
| 74 | + | func TestApplyDefaultHardwareDecodeEnvironment_DefaultsCEFToVAAPIForAuto(t *testing.T) { | |||
| 75 | + | t.Setenv(cefEnableVAAPIEnvVar, "") | |||
| 76 | + | ||||
| 77 | + | ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{ | |||
| 78 | + | Engine: config.EngineConfig{Type: config.EngineTypeCEF}, | |||
| 79 | + | Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto}, | |||
| 80 | + | }) | |||
| 81 | + | ||||
| 82 | + | if got := os.Getenv(cefEnableVAAPIEnvVar); got != "1" { | |||
| 83 | + | t.Fatalf("%s = %q, want 1", cefEnableVAAPIEnvVar, got) | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func TestApplyDefaultHardwareDecodeEnvironment_PreservesExplicitLIBVADriver(t *testing.T) { | |||
| 88 | + | t.Setenv(cefEnableVAAPIEnvVar, "") | |||
| 89 | + | t.Setenv("LIBVA_DRIVER_NAME", "custom-driver") | |||
| 90 | + | ||||
| 91 | + | ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{ | |||
| 92 | + | Engine: config.EngineConfig{Type: config.EngineTypeCEF}, | |||
| 93 | + | Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto}, | |||
| 94 | + | }) | |||
| 95 | + | ||||
| 96 | + | if got := os.Getenv("LIBVA_DRIVER_NAME"); got != "custom-driver" { | |||
| 97 | + | t.Fatalf("LIBVA_DRIVER_NAME = %q, want explicit custom-driver", got) | |||
| 98 | + | } | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | func TestApplyDefaultHardwareDecodeEnvironment_DisablesCEFVAAPIWhenMediaDisabled(t *testing.T) { | |||
| 102 | + | t.Setenv(cefEnableVAAPIEnvVar, "") | |||
| 103 | + | ||||
| 104 | + | ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{ | |||
| 105 | + | Engine: config.EngineConfig{Type: config.EngineTypeCEF}, | |||
| 106 | + | Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingDisable}, | |||
| 107 | + | }) | |||
| 108 | + | ||||
| 109 | + | if got := os.Getenv(cefEnableVAAPIEnvVar); got != "0" { | |||
| 110 | + | t.Fatalf("%s = %q, want 0", cefEnableVAAPIEnvVar, got) | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | func TestApplyDefaultHardwareDecodeEnvironment_PreservesExplicitCEFVAAPIOverride(t *testing.T) { | |||
| 115 | + | t.Setenv(cefEnableVAAPIEnvVar, "0") | |||
| 116 | + | ||||
| 117 | + | ApplyDefaultHardwareDecodeEnvironment(context.Background(), &config.Config{ | |||
| 118 | + | Engine: config.EngineConfig{Type: config.EngineTypeCEF}, | |||
| 119 | + | Media: config.MediaConfig{HardwareDecodingMode: config.HardwareDecodingAuto}, | |||
| 120 | + | }) | |||
| 121 | + | ||||
| 122 | + | if got := os.Getenv(cefEnableVAAPIEnvVar); got != "0" { | |||
| 123 | + | t.Fatalf("%s = %q, want explicit 0", cefEnableVAAPIEnvVar, got) | |||
| 124 | + | } | |||
| 125 | + | } | |||
M internal/infrastructure/cef/webview.go +37 -10
| @@ -1042,16 +1042,7 @@ func (wv *WebView) Destroy() { | |||
| 1042 | 1042 | host.CloseBrowser(1) | |
| 1043 | 1043 | } else { | |
| 1044 | 1044 | wv.runCloseCallbacks() | |
| 1045 | - | } | |
| 1046 | - | if bridge := wv.viewBridge; bridge != nil { | |
| 1047 | - | wv.runOnGTKSync(func() { | |
| 1048 | - | if wv.removeSizeObserver != nil { | |
| 1049 | - | wv.removeSizeObserver() | |
| 1050 | - | wv.removeSizeObserver = nil | |
| 1051 | - | } | |
| 1052 | - | _ = bridge.DetachInput() | |
| 1053 | - | _ = bridge.Destroy() | |
| 1054 | - | }) | |
| 1045 | + | wv.destroyViewBridgeOnGTKSync() | |
| 1055 | 1046 | } | |
| 1056 | 1047 | if wv.profileCleanup != nil { | |
| 1057 | 1048 | wv.profileCleanup() | |
| @@ -1059,6 +1050,42 @@ func (wv *WebView) Destroy() { | |||
| 1059 | 1050 | } | |
| 1060 | 1051 | } | |
| 1061 | 1052 | ||
| 1053 | + | // destroyViewBridgeOnGTKSync, destroyViewBridgeOnGTKAsync, and | |
| 1054 | + | // destroyViewBridgeOnGTKThread may race with one another during teardown. The | |
| 1055 | + | // early viewBridge nil checks avoid scheduling unnecessary GTK work; the final | |
| 1056 | + | // nil guard on the GTK thread is authoritative and makes duplicate scheduling | |
| 1057 | + | // harmless. | |
| 1058 | + | func (wv *WebView) destroyViewBridgeOnGTKSync() { | |
| 1059 | + | if wv == nil || wv.viewBridge == nil { | |
| 1060 | + | return | |
| 1061 | + | } | |
| 1062 | + | wv.runOnGTKSync(func() { | |
| 1063 | + | wv.destroyViewBridgeOnGTKThread() | |
| 1064 | + | }) | |
| 1065 | + | } | |
| 1066 | + | ||
| 1067 | + | func (wv *WebView) destroyViewBridgeOnGTKAsync() { | |
| 1068 | + | if wv == nil || wv.viewBridge == nil { | |
| 1069 | + | return | |
| 1070 | + | } | |
| 1071 | + | wv.runOnGTK(func() { | |
| 1072 | + | wv.destroyViewBridgeOnGTKThread() | |
| 1073 | + | }) | |
| 1074 | + | } | |
| 1075 | + | ||
| 1076 | + | func (wv *WebView) destroyViewBridgeOnGTKThread() { | |
| 1077 | + | if wv == nil || wv.viewBridge == nil { | |
| 1078 | + | return | |
| 1079 | + | } | |
| 1080 | + | if wv.removeSizeObserver != nil { | |
| 1081 | + | wv.removeSizeObserver() | |
| 1082 | + | wv.removeSizeObserver = nil | |
| 1083 | + | } | |
| 1084 | + | _ = wv.viewBridge.DetachInput() | |
| 1085 | + | _ = wv.viewBridge.Destroy() | |
| 1086 | + | wv.viewBridge = nil | |
| 1087 | + | } | |
| 1088 | + | ||
| 1062 | 1089 | // --------------------------------------------------------------------------- | |
| 1063 | 1090 | // NativeWidgetProvider | |
| 1064 | 1091 | // --------------------------------------------------------------------------- | |
| @@ -1042,16 +1042,7 @@ func (wv *WebView) Destroy() { | |||||
| 1042 | host.CloseBrowser(1) | 1042 | host.CloseBrowser(1) | ||
| 1043 | } else { | 1043 | } else { | ||
| 1044 | wv.runCloseCallbacks() | 1044 | wv.runCloseCallbacks() | ||
| 1045 | - | } | 1045 | + | wv.destroyViewBridgeOnGTKSync() |
| 1046 | - | if bridge := wv.viewBridge; bridge != nil { | |||
| 1047 | - | wv.runOnGTKSync(func() { | |||
| 1048 | - | if wv.removeSizeObserver != nil { | |||
| 1049 | - | wv.removeSizeObserver() | |||
| 1050 | - | wv.removeSizeObserver = nil | |||
| 1051 | - | } | |||
| 1052 | - | _ = bridge.DetachInput() | |||
| 1053 | - | _ = bridge.Destroy() | |||
| 1054 | - | }) | |||
| 1055 | } | 1046 | } | ||
| 1056 | if wv.profileCleanup != nil { | 1047 | if wv.profileCleanup != nil { | ||
| 1057 | wv.profileCleanup() | 1048 | wv.profileCleanup() | ||
| @@ -1059,6 +1050,42 @@ func (wv *WebView) Destroy() { | |||||
| 1059 | } | 1050 | } | ||
| 1060 | } | 1051 | } | ||
| 1061 | 1052 | ||||
| 1053 | + | // destroyViewBridgeOnGTKSync, destroyViewBridgeOnGTKAsync, and | |||
| 1054 | + | // destroyViewBridgeOnGTKThread may race with one another during teardown. The | |||
| 1055 | + | // early viewBridge nil checks avoid scheduling unnecessary GTK work; the final | |||
| 1056 | + | // nil guard on the GTK thread is authoritative and makes duplicate scheduling | |||
| 1057 | + | // harmless. | |||
| 1058 | + | func (wv *WebView) destroyViewBridgeOnGTKSync() { | |||
| 1059 | + | if wv == nil || wv.viewBridge == nil { | |||
| 1060 | + | return | |||
| 1061 | + | } | |||
| 1062 | + | wv.runOnGTKSync(func() { | |||
| 1063 | + | wv.destroyViewBridgeOnGTKThread() | |||
| 1064 | + | }) | |||
| 1065 | + | } | |||
| 1066 | + | ||||
| 1067 | + | func (wv *WebView) destroyViewBridgeOnGTKAsync() { | |||
| 1068 | + | if wv == nil || wv.viewBridge == nil { | |||
| 1069 | + | return | |||
| 1070 | + | } | |||
| 1071 | + | wv.runOnGTK(func() { | |||
| 1072 | + | wv.destroyViewBridgeOnGTKThread() | |||
| 1073 | + | }) | |||
| 1074 | + | } | |||
| 1075 | + | ||||
| 1076 | + | func (wv *WebView) destroyViewBridgeOnGTKThread() { | |||
| 1077 | + | if wv == nil || wv.viewBridge == nil { | |||
| 1078 | + | return | |||
| 1079 | + | } | |||
| 1080 | + | if wv.removeSizeObserver != nil { | |||
| 1081 | + | wv.removeSizeObserver() | |||
| 1082 | + | wv.removeSizeObserver = nil | |||
| 1083 | + | } | |||
| 1084 | + | _ = wv.viewBridge.DetachInput() | |||
| 1085 | + | _ = wv.viewBridge.Destroy() | |||
| 1086 | + | wv.viewBridge = nil | |||
| 1087 | + | } | |||
| 1088 | + | ||||
| 1062 | // --------------------------------------------------------------------------- | 1089 | // --------------------------------------------------------------------------- | ||
| 1063 | // NativeWidgetProvider | 1090 | // NativeWidgetProvider | ||
| 1064 | // --------------------------------------------------------------------------- | 1091 | // --------------------------------------------------------------------------- | ||
M internal/infrastructure/cef/webview_handlers.go +3 -1
| @@ -657,7 +657,9 @@ func (h *handlerSet) OnBeforeClose(browser purecef.Browser) { | |||
| 657 | 657 | h.wv.findCtrl.setHost(nil) | |
| 658 | 658 | } | |
| 659 | 659 | h.wv.mu.Unlock() | |
| 660 | - | if h.wv.viewBridge != nil { | |
| 660 | + | if h.wv.destroyed.Load() { | |
| 661 | + | h.wv.destroyViewBridgeOnGTKAsync() | |
| 662 | + | } else if h.wv.viewBridge != nil { | |
| 661 | 663 | wv := h.wv | |
| 662 | 664 | wv.runOnGTK(func() { | |
| 663 | 665 | if wv.viewBridge == nil { | |
| @@ -657,7 +657,9 @@ func (h *handlerSet) OnBeforeClose(browser purecef.Browser) { | |||||
| 657 | h.wv.findCtrl.setHost(nil) | 657 | h.wv.findCtrl.setHost(nil) | ||
| 658 | } | 658 | } | ||
| 659 | h.wv.mu.Unlock() | 659 | h.wv.mu.Unlock() | ||
| 660 | - | if h.wv.viewBridge != nil { | 660 | + | if h.wv.destroyed.Load() { |
| 661 | + | h.wv.destroyViewBridgeOnGTKAsync() | |||
| 662 | + | } else if h.wv.viewBridge != nil { | |||
| 661 | wv := h.wv | 663 | wv := h.wv | ||
| 662 | wv.runOnGTK(func() { | 664 | wv.runOnGTK(func() { | ||
| 663 | if wv.viewBridge == nil { | 665 | if wv.viewBridge == nil { | ||
A internal/infrastructure/cef/webview_lifecycle_test.go +48 -0
| @@ -0,0 +1,48 @@ | |||
| 1 | + | package cef | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | purecef "github.com/bnema/purego-cef/cef" | |
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestDestroy_WithHostDefersBridgeDestroyUntilBeforeClose(t *testing.T) { | |
| 11 | + | bridge := &Cef2gtkAdapter{} | |
| 12 | + | host := &stubBrowserHost{} | |
| 13 | + | wv := &WebView{ | |
| 14 | + | viewBridge: bridge, | |
| 15 | + | host: host, | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | wv.Destroy() | |
| 19 | + | ||
| 20 | + | require.True(t, wv.IsDestroyed()) | |
| 21 | + | require.True(t, host.closeBrowserCalled) | |
| 22 | + | require.False(t, bridge.IsDestroyed(), "Destroy should request CEF close before destroying the GTK bridge") | |
| 23 | + | ||
| 24 | + | wv.destroyViewBridgeOnGTKSync() | |
| 25 | + | ||
| 26 | + | require.True(t, bridge.IsDestroyed()) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | func TestDestroy_WithoutHostDestroysBridgeImmediately(t *testing.T) { | |
| 30 | + | bridge := &Cef2gtkAdapter{} | |
| 31 | + | wv := &WebView{viewBridge: bridge} | |
| 32 | + | ||
| 33 | + | wv.Destroy() | |
| 34 | + | ||
| 35 | + | require.True(t, bridge.IsDestroyed()) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // stubBrowserHost intentionally only stubs CloseBrowser. The embedded | |
| 39 | + | // purecef.BrowserHost may be nil; calling any other BrowserHost method will | |
| 40 | + | // panic and should be avoided in these lifecycle tests. | |
| 41 | + | type stubBrowserHost struct { | |
| 42 | + | purecef.BrowserHost | |
| 43 | + | closeBrowserCalled bool | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func (s *stubBrowserHost) CloseBrowser(int32) { | |
| 47 | + | s.closeBrowserCalled = true | |
| 48 | + | } | |
| @@ -0,0 +1,48 @@ | |||||
| 1 | + | package cef | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | purecef "github.com/bnema/purego-cef/cef" | |||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func TestDestroy_WithHostDefersBridgeDestroyUntilBeforeClose(t *testing.T) { | |||
| 11 | + | bridge := &Cef2gtkAdapter{} | |||
| 12 | + | host := &stubBrowserHost{} | |||
| 13 | + | wv := &WebView{ | |||
| 14 | + | viewBridge: bridge, | |||
| 15 | + | host: host, | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | wv.Destroy() | |||
| 19 | + | ||||
| 20 | + | require.True(t, wv.IsDestroyed()) | |||
| 21 | + | require.True(t, host.closeBrowserCalled) | |||
| 22 | + | require.False(t, bridge.IsDestroyed(), "Destroy should request CEF close before destroying the GTK bridge") | |||
| 23 | + | ||||
| 24 | + | wv.destroyViewBridgeOnGTKSync() | |||
| 25 | + | ||||
| 26 | + | require.True(t, bridge.IsDestroyed()) | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | func TestDestroy_WithoutHostDestroysBridgeImmediately(t *testing.T) { | |||
| 30 | + | bridge := &Cef2gtkAdapter{} | |||
| 31 | + | wv := &WebView{viewBridge: bridge} | |||
| 32 | + | ||||
| 33 | + | wv.Destroy() | |||
| 34 | + | ||||
| 35 | + | require.True(t, bridge.IsDestroyed()) | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // stubBrowserHost intentionally only stubs CloseBrowser. The embedded | |||
| 39 | + | // purecef.BrowserHost may be nil; calling any other BrowserHost method will | |||
| 40 | + | // panic and should be avoided in these lifecycle tests. | |||
| 41 | + | type stubBrowserHost struct { | |||
| 42 | + | purecef.BrowserHost | |||
| 43 | + | closeBrowserCalled bool | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func (s *stubBrowserHost) CloseBrowser(int32) { | |||
| 47 | + | s.closeBrowserCalled = true | |||
| 48 | + | } | |||
M internal/infrastructure/config/loader.go +0 -2
| @@ -610,8 +610,6 @@ func (m *Manager) setEngineDefaults(defaults *Config) { | |||
| 610 | 610 | m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold) | |
| 611 | 611 | } | |
| 612 | 612 | ||
| 613 | - | ||
| 614 | - | ||
| 615 | 613 | // New returns a new default configuration instance. | |
| 616 | 614 | // This is a convenience function for getting default config without the full manager. | |
| 617 | 615 | func New() *Config { | |
| @@ -610,8 +610,6 @@ func (m *Manager) setEngineDefaults(defaults *Config) { | |||||
| 610 | m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold) | 610 | m.viper.SetDefault("engine.webkit.network_process_memory_strict_threshold", wk.NetworkProcessMemoryStrictThreshold) | ||
| 611 | } | 611 | } | ||
| 612 | 612 | ||||
| 613 | - | ||||
| 614 | - | ||||
| 615 | // New returns a new default configuration instance. | 613 | // New returns a new default configuration instance. | ||
| 616 | // This is a convenience function for getting default config without the full manager. | 614 | // This is a convenience function for getting default config without the full manager. | ||
| 617 | func New() *Config { | 615 | func New() *Config { | ||
M internal/ui/app.go +16 -2
| @@ -2581,9 +2581,23 @@ func (a *App) MainWindow() *window.MainWindow { | |||
| 2581 | 2581 | ||
| 2582 | 2582 | // Quit requests the application to quit. | |
| 2583 | 2583 | func (a *App) Quit() { | |
| 2584 | - | if a.gtkApp != nil { | |
| 2585 | - | a.gtkApp.Quit() | |
| 2584 | + | if a == nil || a.gtkApp == nil { | |
| 2585 | + | return | |
| 2586 | + | } | |
| 2587 | + | quit := func() { | |
| 2588 | + | if a.gtkApp != nil { | |
| 2589 | + | a.gtkApp.Quit() | |
| 2590 | + | } | |
| 2591 | + | } | |
| 2592 | + | glibCtx := glib.MainContextDefault() | |
| 2593 | + | if glibCtx != nil && glibCtx.IsOwner() { | |
| 2594 | + | quit() | |
| 2595 | + | return | |
| 2586 | 2596 | } | |
| 2597 | + | cb := glib.SourceOnceFunc(func(_ uintptr) { | |
| 2598 | + | quit() | |
| 2599 | + | }) | |
| 2600 | + | glib.IdleAddOnce(&cb, 0) | |
| 2587 | 2601 | } | |
| 2588 | 2602 | ||
| 2589 | 2603 | // RunWithArgs is a convenience function that creates and runs an App. | |
| @@ -2581,9 +2581,23 @@ func (a *App) MainWindow() *window.MainWindow { | |||||
| 2581 | 2581 | ||||
| 2582 | // Quit requests the application to quit. | 2582 | // Quit requests the application to quit. | ||
| 2583 | func (a *App) Quit() { | 2583 | func (a *App) Quit() { | ||
| 2584 | - | if a.gtkApp != nil { | 2584 | + | if a == nil || a.gtkApp == nil { |
| 2585 | - | a.gtkApp.Quit() | 2585 | + | return |
| 2586 | + | } | |||
| 2587 | + | quit := func() { | |||
| 2588 | + | if a.gtkApp != nil { | |||
| 2589 | + | a.gtkApp.Quit() | |||
| 2590 | + | } | |||
| 2591 | + | } | |||
| 2592 | + | glibCtx := glib.MainContextDefault() | |||
| 2593 | + | if glibCtx != nil && glibCtx.IsOwner() { | |||
| 2594 | + | quit() | |||
| 2595 | + | return | |||
| 2586 | } | 2596 | } | ||
| 2597 | + | cb := glib.SourceOnceFunc(func(_ uintptr) { | |||
| 2598 | + | quit() | |||
| 2599 | + | }) | |||
| 2600 | + | glib.IdleAddOnce(&cb, 0) | |||
| 2587 | } | 2601 | } | ||
| 2588 | 2602 | ||||
| 2589 | // RunWithArgs is a convenience function that creates and runs an App. | 2603 | // RunWithArgs is a convenience function that creates and runs an App. | ||