Files
OpsLog/webview2_windows.go
rouggy 14bb92fac0 feat(startup): name an Edge blocker instead of hanging
Confirmed on the machine that started this: an 'Edge blocker' utility was
in the way, and unblocking Edge fixed the launch.

Those tools work by registering a Debugger entry under Image File
Execution Options for msedge.exe and msedgewebview2.exe, so Windows
refuses to run the process. The runtime is installed and registers its
version quite happily — which is why the logs showed WebView2 present —
and then never starts. OpsLog draws its whole interface with that engine,
so the window never opened: no error, no crash, a process sitting there
doing nothing.

That entry is read at startup now and said plainly, in the log and on
screen, with the one thing that helps: unblock Edge in the tool that
blocked it. Reinstalling OpsLog cannot fix a machine in this state, and
that is where an unexplained silence sends people.

Also logs the Windows build and which Edge policies are set — value names
only, never their contents: a policy key holds URLs and account names
that are nobody's business here.
2026-08-25 21:19:37 +02:00

138 lines
4.9 KiB
Go

//go:build windows
package main
// Is the WebView2 runtime actually installed?
//
// It is the one dependency OpsLog cannot ship inside its own executable, and
// its absence is invisible: the process starts, Wails fails to create the
// browser environment, and on some machines that failure arrives as a silent
// exit rather than as an error we can print. So the version is read straight
// from where the runtime registers itself, and written to the startup log
// BEFORE the window is attempted — a log that says "WebView2: not found"
// answers the whole question in one line.
import (
"fmt"
"strings"
"golang.org/x/sys/windows/registry"
)
// webview2Client is the runtime's own update-client GUID, the key it registers
// its version under. Documented by Microsoft for exactly this check.
const webview2Client = `Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}`
// webView2Version returns the installed runtime version, or "" if there is none.
//
// Both hives: a per-machine install writes to HKLM (and to the WOW6432 view of
// it on 64-bit Windows, which is where it usually lands), a per-user install to
// HKCU. Missing all three is the answer that matters.
func webView2Version() string {
type place struct {
key registry.Key
access uint32
}
for _, p := range []place{
{registry.CURRENT_USER, registry.QUERY_VALUE},
{registry.LOCAL_MACHINE, registry.QUERY_VALUE},
{registry.LOCAL_MACHINE, registry.QUERY_VALUE | registry.WOW64_32KEY},
} {
k, err := registry.OpenKey(p.key, webview2Client, p.access)
if err != nil {
continue
}
v, _, err := k.GetStringValue("pv")
k.Close()
if err == nil && v != "" && v != "0.0.0.0" {
return v
}
}
return ""
}
// windowsVersion reads the build from the registry, for the startup log.
//
// Not curiosity: "WebView2 will not install either" is a statement about the
// machine, not about OpsLog, and the build number is the first thing anyone
// diagnosing that will ask for.
func windowsVersion() string {
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
if err != nil {
return "unknown"
}
defer k.Close()
name, _, _ := k.GetStringValue("ProductName")
build, _, _ := k.GetStringValue("CurrentBuild")
ubr, _, _ := k.GetIntegerValue("UBR")
display, _, _ := k.GetStringValue("DisplayVersion")
return fmt.Sprintf("%s %s build %s.%d", name, display, build, ubr)
}
// edgePolicyNotes reports the administrative policies applied to Edge, which is
// the engine WebView2 runs on.
//
// "Edge is blocked on this PC" is a sentence about group policy, and policy is
// readable: rather than have an operator guess which rule is in the way, the
// names of the values under the Edge policy key go into the startup log. It is
// evidence for whoever administers that machine, in the one file we can be sure
// they will be sent.
//
// Value NAMES only, never their contents: a policy key can hold URLs, account
// names and site lists that are nobody's business here.
func edgePolicyNotes() string {
var found []string
for _, hive := range []registry.Key{registry.LOCAL_MACHINE, registry.CURRENT_USER} {
k, err := registry.OpenKey(hive, `SOFTWARE\Policies\Microsoft\Edge`, registry.QUERY_VALUE|registry.ENUMERATE_SUB_KEYS)
if err != nil {
continue
}
names, _ := k.ReadValueNames(0)
subs, _ := k.ReadSubKeyNames(0)
k.Close()
where := "HKLM"
if hive == registry.CURRENT_USER {
where = "HKCU"
}
if len(names) > 0 || len(subs) > 0 {
found = append(found, fmt.Sprintf("%s: %d policy values, %d sub-keys %v", where, len(names), len(subs), names))
}
}
if len(found) == 0 {
return "no Edge policies set"
}
return strings.Join(found, " | ")
}
// edgeExecutionBlocked reports an Image File Execution Options entry that stops
// the WebView2 process from running.
//
// This is what the "Edge blocker" utilities do: they register a Debugger value
// under IFEO for msedge.exe and msedgewebview2.exe, which makes Windows launch
// something else — usually nothing — in its place. The WebView2 runtime is
// installed, it registers its version, and it never starts, which is exactly
// the hang OpsLog was showing. Confirmed on a real machine: an Edge blocker was
// in the way, and unblocking it fixed the launch.
//
// One line in the startup log instead of an afternoon.
func edgeExecutionBlocked() string {
const ifeo = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\`
var hits []string
for _, exe := range []string{"msedge.exe", "msedgewebview2.exe"} {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, ifeo+exe, registry.QUERY_VALUE)
if err != nil {
continue
}
dbg, _, err := k.GetStringValue("Debugger")
k.Close()
if err == nil && strings.TrimSpace(dbg) != "" {
hits = append(hits, fmt.Sprintf("%s → Debugger=%q", exe, dbg))
}
}
if len(hits) == 0 {
return ""
}
return strings.Join(hits, " | ")
}