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 "" }