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.
242 lines
10 KiB
Go
242 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"embed"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/wailsapp/wails/v2"
|
|
"github.com/wailsapp/wails/v2/pkg/options"
|
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
|
wailswindows "github.com/wailsapp/wails/v2/pkg/options/windows"
|
|
)
|
|
|
|
//go:embed all:frontend/dist
|
|
var assets embed.FS
|
|
|
|
// profileArg extracts a profile name from the command line. Accepts
|
|
// "--profile NAME", "--profile=NAME", "-profile NAME", "-p NAME" so a desktop
|
|
// shortcut can launch OpsLog straight into a given profile (e.g. F4BPO / TM2Q).
|
|
func profileArg(args []string) string {
|
|
for i := 0; i < len(args); i++ {
|
|
a := args[i]
|
|
switch {
|
|
case a == "--profile" || a == "-profile" || a == "-p":
|
|
if i+1 < len(args) {
|
|
return strings.TrimSpace(args[i+1])
|
|
}
|
|
case strings.HasPrefix(a, "--profile="):
|
|
return strings.TrimSpace(strings.TrimPrefix(a, "--profile="))
|
|
case strings.HasPrefix(a, "-profile="):
|
|
return strings.TrimSpace(strings.TrimPrefix(a, "-profile="))
|
|
case strings.HasPrefix(a, "-p="):
|
|
return strings.TrimSpace(strings.TrimPrefix(a, "-p="))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// hasFlag reports whether flag is present in args.
|
|
func hasFlag(args []string, flag string) bool {
|
|
for _, a := range args {
|
|
if a == flag {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// acquireInstance grabs the single-instance mutex. On a normal launch it's a plain
|
|
// try (fail → another OpsLog is running, so exit). On a --post-update relaunch the
|
|
// previous instance may still be shutting down and holding the mutex, so retry for
|
|
// a few seconds until it frees.
|
|
func acquireInstance(postUpdate bool) bool {
|
|
if acquireSingleInstance() {
|
|
return true
|
|
}
|
|
if !postUpdate {
|
|
return false
|
|
}
|
|
deadline := time.Now().Add(20 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
time.Sleep(300 * time.Millisecond)
|
|
if acquireSingleInstance() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// processStart is stamped on the very first instruction of main, before the
|
|
// single-instance guard and before anything else runs.
|
|
//
|
|
// It exists to split a slow launch in two, because the log alone could not: the
|
|
// first line applog writes already sits inside startup(), so everything spent
|
|
// loading the binary and creating the WebView2 environment happened before the
|
|
// log begins and was invisible. An operator reporting "nothing happens for three
|
|
// seconds" was impossible to answer from a file whose first timestamp is the
|
|
// moment the app was already running.
|
|
var processStart = time.Now()
|
|
|
|
func main() {
|
|
// Single-instance guard: if OpsLog is already running, focus that window and
|
|
// exit instead of spawning a duplicate. A second process would open its own
|
|
// CAT (FlexRadio) connection and Ultrabeam follow loop, and the two would
|
|
// fight over the rig/antenna frequency — the cause of "the antenna re-tunes on
|
|
// its own" when a windowless zombie instance was left running.
|
|
// A --post-update relaunch (from the auto-updater) may start while the previous
|
|
// instance is still exiting and holding the single-instance mutex — wait for it
|
|
// to free instead of bailing out. Then clear the old exe it left behind.
|
|
bootLogLaunch()
|
|
postUpdate := hasFlag(os.Args[1:], "--post-update")
|
|
if !acquireInstance(postUpdate) {
|
|
// SAID, not merely done. This exit is correct — a second instance would
|
|
// fight the first over the rig — but it happened in total silence: no
|
|
// window, no data folder, no log, which is indistinguishable from a
|
|
// program that died on its first instruction.
|
|
bootLog("another instance already holds the single-instance mutex - exiting")
|
|
fatalBox("OpsLog", "OpsLog is already running.\n\nLook for its window, or for a leftover OpsLog.exe in the Task Manager, and close it before starting another.")
|
|
return
|
|
}
|
|
bootLog("single-instance mutex acquired")
|
|
|
|
// The data folder is created BESIDE the executable, so a copy dropped into
|
|
// Program Files is refused the write by Windows — and that refusal ended the
|
|
// launch with nothing on screen and nothing in any file, since the only log
|
|
// OpsLog had was inside the folder it could not create.
|
|
if err := checkDataDirWritable(); err != nil {
|
|
bootLog("FATAL %v", err)
|
|
fatalBox("OpsLog", "OpsLog cannot write next to its own program file.\n\n"+err.Error()+
|
|
"\n\nMove OpsLog.exe somewhere your account can write — a folder in Documents, or the desktop — and start it again. Program Files is refused to anything not running as administrator.")
|
|
return
|
|
}
|
|
if postUpdate {
|
|
cleanupOldUpdateBinary()
|
|
}
|
|
|
|
// The WebView2 runtime, named before the window is attempted. Its absence is
|
|
// the classic silent failure — the process starts, the browser environment
|
|
// cannot be created, and on some machines that ends the launch without an
|
|
// error anyone can see.
|
|
if v := webView2Version(); v != "" {
|
|
bootLog("WebView2 runtime: %s", v)
|
|
} else {
|
|
bootLog("WebView2 runtime: NOT FOUND — install Microsoft Edge WebView2 Runtime")
|
|
}
|
|
|
|
// Create an instance of the app structure
|
|
app := NewApp()
|
|
app.startupProfile = profileArg(os.Args[1:])
|
|
|
|
// Restore the window's SIZE and maximised state at CREATION, from the geometry
|
|
// saved on last close. Doing it here (not after startup) is what makes the
|
|
// window open already at the right size instead of maximising then snapping
|
|
// smaller. Position can't be set through options, so it is applied while the
|
|
// window is still hidden (domReady) — invisible, so no jump. First run, or a
|
|
// window closed maximised, keeps the historical maximised default.
|
|
width, height := 1400, 900
|
|
startState := options.Maximised
|
|
if dataDir, err := userDataDir(); err == nil {
|
|
if ws, ok := readWindowState(dataDir); ok && !ws.Maximised &&
|
|
ws.Width >= 1100 && ws.Height >= 700 && ws.Width <= 8000 && ws.Height <= 6000 {
|
|
width, height = ws.Width, ws.Height
|
|
startState = options.Normal
|
|
}
|
|
}
|
|
|
|
// Create application with options
|
|
// A HANG has no error to report, and that is the shape being chased here:
|
|
// the process stays up, the data folder is created and stays empty, and no
|
|
// window appears — WebView2 never finishes creating its environment, and
|
|
// nothing returns to be logged.
|
|
//
|
|
// So the absence of progress is what gets reported. If OnStartup has not
|
|
// been reached a few seconds in, this says so and names the two things that
|
|
// cause it, rather than leaving an operator watching a process that is doing
|
|
// nothing at all.
|
|
go func() {
|
|
time.Sleep(12 * time.Second)
|
|
if startupReached.Load() {
|
|
return
|
|
}
|
|
bootLog("STUCK: 12 s after wails.Run and the window has not started — WebView2 never handed control back")
|
|
fatalBox("OpsLog", startupStuckMessage)
|
|
}()
|
|
noteLaunchAttempt()
|
|
bootLog("Windows %s", windowsVersion())
|
|
bootLog("Edge policy: %s", edgePolicyNotes())
|
|
// The blocker utilities: they leave an IFEO entry that makes Windows refuse
|
|
// to run the WebView2 process at all. Named loudly, because a machine in
|
|
// this state looks identical to one with a broken graphics driver.
|
|
if blocked := edgeExecutionBlocked(); blocked != "" {
|
|
bootLog("EDGE EXECUTION IS BLOCKED: %s — an \"Edge blocker\" tool is stopping WebView2 from starting", blocked)
|
|
fatalBox("OpsLog", edgeBlockedMessage)
|
|
}
|
|
bootLog("WebView2 profile: %q", webviewDataPath())
|
|
bootLog("entering wails.Run (window %dx%d, state %v)", width, height, startState)
|
|
err := wails.Run(&options.App{
|
|
Title: "OpsLog",
|
|
Width: width,
|
|
Height: height,
|
|
MinWidth: 1100,
|
|
MinHeight: 700,
|
|
WindowStartState: startState,
|
|
// No OS title bar: it was a dead 32-pixel band above a window that already
|
|
// has its own title strip. The app header takes over — it carries the drag
|
|
// region, the double-click-to-maximise, and the minimise/maximise/close
|
|
// buttons. Windows still draws the resize borders and honours Aero Snap,
|
|
// which is why the frame is dropped rather than the whole chrome.
|
|
Frameless: true,
|
|
// Start hidden and reveal only once the saved position has been applied and
|
|
// the DOM has painted (OnDomReady → domReady) — so the window appears
|
|
// already at its final size and position, with no post-launch jump.
|
|
StartHidden: true,
|
|
AssetServer: &assetserver.Options{
|
|
Assets: assets,
|
|
},
|
|
// The colour the window is painted before the WebView draws. Matches the
|
|
// graphite theme's background (#16181d), which is the default on a fresh
|
|
// install — otherwise every launch flashes white first.
|
|
BackgroundColour: &options.RGBA{R: 0x16, G: 0x18, B: 0x1d, A: 1},
|
|
// WEBVIEW2, pinned down.
|
|
//
|
|
// Its profile folder is normally chosen by Wails under the roaming
|
|
// profile — a place that on a managed or redirected account can be slow,
|
|
// locked or simply gone, and the failure mode is a creation that never
|
|
// returns rather than an error. Naming it puts it on the local disk,
|
|
// beside the startup log, where it can also be DELETED when it is the
|
|
// thing that is broken.
|
|
//
|
|
// GPU acceleration is switched off only when the previous launch hung
|
|
// (see stuckMarker): a WebView2 that cannot get on with the graphics
|
|
// driver hangs exactly like one that cannot start at all, and this is the
|
|
// one lever that separates the two — without costing anything on the
|
|
// machines where it was never the problem.
|
|
Windows: &wailswindows.Options{
|
|
WebviewUserDataPath: webviewDataPath(),
|
|
// A fixed-version runtime carried beside OpsLog, when one is there.
|
|
// Empty means the installed Evergreen runtime, as before.
|
|
WebviewBrowserPath: fixedWebView2Path(),
|
|
WebviewGpuIsDisabled: lastLaunchHung,
|
|
},
|
|
OnStartup: app.startup,
|
|
OnDomReady: app.domReady,
|
|
OnBeforeClose: app.beforeClose,
|
|
OnShutdown: app.shutdown,
|
|
Bind: []interface{}{
|
|
app,
|
|
},
|
|
})
|
|
|
|
bootLog("wails.Run returned")
|
|
if err != nil {
|
|
// The last thing that can fail before a window exists, and the most
|
|
// opaque of them: a missing WebView2 runtime lands here. println goes
|
|
// nowhere in a GUI-subsystem program, so this went unseen and unlogged.
|
|
bootLog("FATAL wails.Run: %v", err)
|
|
fatalBox("OpsLog", "OpsLog could not open its window.\n\n"+err.Error()+
|
|
"\n\nThis is usually a missing WebView2 runtime — install \"Microsoft Edge WebView2 Runtime\" and try again.")
|
|
}
|
|
}
|