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.
227 lines
8.4 KiB
Go
227 lines
8.4 KiB
Go
package main
|
|
|
|
// The first breadcrumbs, written before anything else can fail.
|
|
//
|
|
// OpsLog keeps its log in the data folder, which lives beside the executable —
|
|
// so every fault that happens BEFORE that folder exists is invisible. Reported
|
|
// from a Windows 10 machine: "the process appears, no data folder is created,
|
|
// nothing starts", with no file anywhere to say why. There was nothing to read
|
|
// because the only place we write to had not been created yet.
|
|
//
|
|
// This writes to %LOCALAPPDATA%\OpsLog\startup.log instead: a folder Windows
|
|
// guarantees is writable for the user, whatever OpsLog itself was installed
|
|
// into. It records the handful of milestones between the process starting and
|
|
// the window appearing, and nothing else — it is not a second log, it is the
|
|
// answer to "it does not start".
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// bootLogPath is the file, or "" when even LOCALAPPDATA is unavailable.
|
|
func bootLogPath() string {
|
|
dir := os.Getenv("LOCALAPPDATA")
|
|
if strings.TrimSpace(dir) == "" {
|
|
dir = os.TempDir()
|
|
}
|
|
if dir == "" {
|
|
return ""
|
|
}
|
|
dir = filepath.Join(dir, "OpsLog")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return ""
|
|
}
|
|
return filepath.Join(dir, "startup.log")
|
|
}
|
|
|
|
// bootLog appends one line. Never fails loudly: it exists to explain a failure,
|
|
// so it must not become one.
|
|
func bootLog(format string, args ...any) {
|
|
p := bootLogPath()
|
|
if p == "" {
|
|
return
|
|
}
|
|
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
// Trimmed when it gets long. A startup log that grows for two years is a
|
|
// file nobody opens, and the interesting launch is always the last one.
|
|
if fi, err := f.Stat(); err == nil && fi.Size() > 256*1024 {
|
|
f.Close()
|
|
_ = os.Remove(p)
|
|
f, err = os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
}
|
|
fmt.Fprintf(f, "%s %s\n", time.Now().Format("2006-01-02 15:04:05.000"), fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// bootLogLaunch records what was launched and from where.
|
|
func bootLogLaunch() {
|
|
exe, _ := os.Executable()
|
|
bootLog("launch: %s %v", exe, os.Args[1:])
|
|
}
|
|
|
|
// checkDataDirWritable makes sure the folder OpsLog keeps everything in can
|
|
// actually be created and written to, and says exactly what failed if not.
|
|
//
|
|
// The data folder sits beside the executable, which is fine on a stick or in a
|
|
// home directory and refused outright under Program Files — where Windows
|
|
// silently denies the write to anything not elevated. That refusal used to end
|
|
// the launch with no window and no message.
|
|
func checkDataDirWritable() error {
|
|
dir, err := userDataDir()
|
|
if err != nil {
|
|
return fmt.Errorf("cannot work out where to keep the data: %w", err)
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("cannot create the data folder %s: %w", dir, err)
|
|
}
|
|
probe := filepath.Join(dir, ".writetest")
|
|
if err := os.WriteFile(probe, []byte("ok"), 0o644); err != nil {
|
|
return fmt.Errorf("the data folder %s cannot be written to: %w", dir, err)
|
|
}
|
|
_ = os.Remove(probe)
|
|
bootLog("data dir ok: %s", dir)
|
|
return nil
|
|
}
|
|
|
|
// startupReached flips as soon as OnStartup runs — the first moment OpsLog's
|
|
// own code is executing inside the window's lifecycle.
|
|
var startupReached atomic.Bool
|
|
|
|
// startupStuckMessage is what an operator sees when the window never starts.
|
|
//
|
|
// It names the two causes and what to do about each, because "OpsLog is not
|
|
// responding" sends people to reinstall OpsLog, which is the one thing that
|
|
// cannot help: the part that has not started is not ours.
|
|
const startupStuckMessage = "OpsLog started but its window never opened.\n\n" +
|
|
"This is the WebView2 runtime failing to start, and it is almost always one of two things:\n\n" +
|
|
"• The Microsoft Edge WebView2 Runtime is missing — install it from Microsoft, then start OpsLog again.\n" +
|
|
"• An antivirus is blocking msedgewebview2.exe — add OpsLog's folder to its exceptions.\n\n" +
|
|
"The details are in the OpsLog folder under LOCALAPPDATA (startup.log)."
|
|
|
|
// webviewDataPath is where WebView2 keeps its profile.
|
|
//
|
|
// Named rather than left to the default, which lands in the ROAMING profile: on
|
|
// a managed account that folder can be redirected to a network share, and a
|
|
// WebView2 profile on a share that is slow or unreachable does not fail — it
|
|
// hangs, which is indistinguishable from a runtime that will not start.
|
|
//
|
|
// It also gives the folder a name someone can be told to delete: a corrupt
|
|
// profile is a real cause of this, and "delete this folder and try again" is
|
|
// only advice if the folder can be pointed at.
|
|
func webviewDataPath() string {
|
|
dir := os.Getenv("LOCALAPPDATA")
|
|
if strings.TrimSpace(dir) == "" {
|
|
return "" // let Wails decide; there is nothing better to offer
|
|
}
|
|
p := filepath.Join(dir, "OpsLog", "WebView2")
|
|
if err := os.MkdirAll(p, 0o755); err != nil {
|
|
bootLog("WebView2 profile folder %s could not be created: %v — leaving it to Wails", p, err)
|
|
return ""
|
|
}
|
|
return p
|
|
}
|
|
|
|
// stuckMarkerPath is written before the window is attempted and removed once it
|
|
// opens, so the NEXT launch can tell that the last one never got there.
|
|
func stuckMarkerPath() string {
|
|
dir := os.Getenv("LOCALAPPDATA")
|
|
if strings.TrimSpace(dir) == "" {
|
|
dir = os.TempDir()
|
|
}
|
|
return filepath.Join(dir, "OpsLog", ".launching")
|
|
}
|
|
|
|
// lastLaunchHung is set at startup from the marker left by the previous run.
|
|
var lastLaunchHung bool
|
|
|
|
// noteLaunchAttempt records that a window is about to be attempted, and reports
|
|
// whether the previous attempt ever finished.
|
|
func noteLaunchAttempt() {
|
|
p := stuckMarkerPath()
|
|
if _, err := os.Stat(p); err == nil {
|
|
lastLaunchHung = true
|
|
bootLog("the previous launch never opened its window — trying again with GPU acceleration off")
|
|
}
|
|
_ = os.MkdirAll(filepath.Dir(p), 0o755)
|
|
_ = os.WriteFile(p, []byte(time.Now().Format(time.RFC3339)), 0o644)
|
|
}
|
|
|
|
// clearLaunchMarker is called once the window is up: the attempt finished.
|
|
func clearLaunchMarker() { _ = os.Remove(stuckMarkerPath()) }
|
|
|
|
// fixedWebView2Path finds a FIXED-VERSION WebView2 runtime shipped beside
|
|
// OpsLog, or "" when there is none.
|
|
//
|
|
// Microsoft publishes the runtime in two forms. Evergreen installs itself into
|
|
// Windows and updates itself — the normal case, and the one this looks for
|
|
// first. FIXED VERSION is a folder of files an application carries with it and
|
|
// points at, needing no installation and no administrator: it exists precisely
|
|
// for machines where Evergreen cannot be installed, which is not a rare
|
|
// situation on a managed, offline or otherwise locked-down Windows.
|
|
//
|
|
// So: drop the extracted folder next to OpsLog.exe and it is used. Nothing to
|
|
// configure — a setting for this would be a question asked of the one operator
|
|
// least able to answer it, on a machine where nothing starts.
|
|
func fixedWebView2Path() string {
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
base := filepath.Dir(exe)
|
|
for _, name := range []string{"WebView2", "WebView2Runtime", "webview2"} {
|
|
dir := filepath.Join(base, name)
|
|
if p := findWebView2Binary(dir); p != "" {
|
|
bootLog("using the fixed-version WebView2 runtime in %s", p)
|
|
return p
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// findWebView2Binary returns dir (or its single versioned subfolder) when it
|
|
// holds msedgewebview2.exe — the fixed-version download unpacks with the
|
|
// version as a folder level, and asking someone to flatten it by hand is one
|
|
// more step to get wrong.
|
|
func findWebView2Binary(dir string) string {
|
|
if fileExists(filepath.Join(dir, "msedgewebview2.exe")) {
|
|
return dir
|
|
}
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
sub := filepath.Join(dir, e.Name())
|
|
if fileExists(filepath.Join(sub, "msedgewebview2.exe")) {
|
|
return sub
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// edgeBlockedMessage is shown when an Edge blocker is found in the way.
|
|
//
|
|
// It names the tool rather than the symptom: OpsLog draws its whole interface
|
|
// with the WebView2 engine, which is Edge's, so a machine where Edge is blocked
|
|
// cannot run it — and no amount of reinstalling OpsLog will change that.
|
|
const edgeBlockedMessage = "OpsLog cannot start because Edge is blocked on this PC.\n\n" +
|
|
"A blocker tool (or a policy) is stopping msedgewebview2.exe from running. " +
|
|
"OpsLog draws its whole interface with that engine, so it cannot open its window.\n\n" +
|
|
"Unblock Edge — in the tool that blocked it — and start OpsLog again. " +
|
|
"The details are in the OpsLog folder under LOCALAPPDATA (startup.log)."
|