fix(startup): pin the WebView2 profile, and retry without the GPU after a hang
His log reaches 'entering wails.Run' and stops: WebView2 is installed (120.0.2210.91) and never hands control back. A hang, not a failure — there is no error to report, so the two remaining causes have to be addressed rather than diagnosed. The profile folder is now named explicitly, on the LOCAL disk. Wails defaults it into the ROAMING profile, which on a managed account can be redirected to a share; a WebView2 profile on a share that is slow or gone does not fail, it hangs. Naming it also gives someone a folder they can be told to delete, which is the fix when the profile itself is corrupt. And a marker is written before the window is attempted, removed when it paints. Finding it at the next launch means the last one never got there, so that launch runs with GPU acceleration off: a WebView2 that cannot get on with the graphics driver hangs exactly like one that cannot start, and this is the single lever that separates them — at no cost on machines where it was never the problem.
This commit is contained in:
@@ -1560,6 +1560,7 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
// launch. Show is unconditional: whatever happens above, the window must appear.
|
// launch. Show is unconditional: whatever happens above, the window must appear.
|
||||||
func (a *App) domReady(ctx context.Context) {
|
func (a *App) domReady(ctx context.Context) {
|
||||||
bootLog("OnDomReady reached — the window has painted")
|
bootLog("OnDomReady reached — the window has painted")
|
||||||
|
clearLaunchMarker()
|
||||||
a.restoreWindowPosition()
|
a.restoreWindowPosition()
|
||||||
wruntime.WindowShow(ctx)
|
wruntime.WindowShow(ctx)
|
||||||
// The one number that matches what the operator actually experiences: click
|
// The one number that matches what the operator actually experiences: click
|
||||||
|
|||||||
+51
@@ -109,3 +109,54 @@ const startupStuckMessage = "OpsLog started but its window never opened.\n\n" +
|
|||||||
"• The Microsoft Edge WebView2 Runtime is missing — install it from Microsoft, then start OpsLog again.\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" +
|
"• 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)."
|
"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()) }
|
||||||
|
|||||||
+4
-2
@@ -17,7 +17,8 @@
|
|||||||
"The satellite dropdown now follows the list as it is edited: the field read it once when the panel first appeared, so a list saved afterwards only showed up after a restart.",
|
"The satellite dropdown now follows the list as it is edited: the field read it once when the panel first appeared, so a list saved afterwards only showed up after a restart.",
|
||||||
"Preferences no longer redraw with the main window. Being a child of the main view, the whole panel was rebuilt on every cluster spot and every CAT update — several times a second on a busy evening — which looked like a page refreshing constantly and made buttons miss their clicks, the element under the pointer being replaced between the press and the release.",
|
"Preferences no longer redraw with the main window. Being a child of the main view, the whole panel was rebuilt on every cluster spot and every CAT update — several times a second on a busy evening — which looked like a page refreshing constantly and made buttons miss their clicks, the element under the pointer being replaced between the press and the release.",
|
||||||
"Dropdown lists open over whatever is beneath them instead of being cut off by it. The satellite list showed one entry of eight, clipped by the panel it sat in.",
|
"Dropdown lists open over whatever is beneath them instead of being cut off by it. The satellite list showed one entry of eight, clipped by the panel it sat in.",
|
||||||
"A launch that fails before the window exists now says why. OpsLog keeps its log in the data folder beside the program, so anything that went wrong BEFORE that folder existed left nothing to read anywhere — the process appeared and vanished in silence. Startup milestones are written to %LOCALAPPDATA%OpsLogstartup.log, and the three silent exits (already running, the data folder cannot be written — Program Files refuses it — and the window itself failing to open, usually a missing WebView2) now show a message saying which one it was. The log also names the installed WebView2 runtime — the one dependency OpsLog cannot carry inside itself, and whose absence is otherwise invisible — and marks each milestone up to the window painting, so a launch that produces nothing can be placed exactly."
|
"A launch that fails before the window exists now says why. OpsLog keeps its log in the data folder beside the program, so anything that went wrong BEFORE that folder existed left nothing to read anywhere — the process appeared and vanished in silence. Startup milestones are written to %LOCALAPPDATA%OpsLogstartup.log, and the three silent exits (already running, the data folder cannot be written — Program Files refuses it — and the window itself failing to open, usually a missing WebView2) now show a message saying which one it was. The log also names the installed WebView2 runtime — the one dependency OpsLog cannot carry inside itself, and whose absence is otherwise invisible — and marks each milestone up to the window painting, so a launch that produces nothing can be placed exactly.",
|
||||||
|
"WebView2 is given an explicit profile folder on the local disk (LOCALAPPDATAOpsLogWebView2) instead of the roaming default, which on a redirected or managed account can hang rather than fail. And when a launch never opens its window, the next one retries with GPU acceleration off and says so — a WebView2 that cannot get on with the graphics driver hangs exactly like one that cannot start at all, and that is the one lever telling them apart."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Console Elecraft : les mesures d'émission sont lues dès que la RADIO se déclare en émission, et plus seulement quand OpsLog l'a mise en émission. Passer en émission par le PTT de façade, une pédale ou le bouton du micro laissait le panneau croire à une réception — et comme les barres de puissance et de ROS ne sont lues qu'en émission, elles ne l'étaient jamais pour qui manipule à la main.",
|
"Console Elecraft : les mesures d'émission sont lues dès que la RADIO se déclare en émission, et plus seulement quand OpsLog l'a mise en émission. Passer en émission par le PTT de façade, une pédale ou le bouton du micro laissait le panneau croire à une réception — et comme les barres de puissance et de ROS ne sont lues qu'en émission, elles ne l'étaient jamais pour qui manipule à la main.",
|
||||||
@@ -34,7 +35,8 @@
|
|||||||
"La liste déroulante des satellites suit maintenant la liste au fil des modifications : le champ la lisait une seule fois à l'apparition du panneau, si bien qu'une liste enregistrée ensuite n'apparaissait qu'après un redémarrage.",
|
"La liste déroulante des satellites suit maintenant la liste au fil des modifications : le champ la lisait une seule fois à l'apparition du panneau, si bien qu'une liste enregistrée ensuite n'apparaissait qu'après un redémarrage.",
|
||||||
"Les préférences ne se redessinent plus au rythme de la fenêtre principale. Étant un enfant de la vue principale, tout le panneau était reconstruit à chaque spot du cluster et à chaque mise à jour CAT — plusieurs fois par seconde un soir chargé — ce qui donnait l'impression d'une page qui se rafraîchit sans arrêt et faisait rater les clics, l'élément sous le pointeur étant remplacé entre l'appui et le relâchement.",
|
"Les préférences ne se redessinent plus au rythme de la fenêtre principale. Étant un enfant de la vue principale, tout le panneau était reconstruit à chaque spot du cluster et à chaque mise à jour CAT — plusieurs fois par seconde un soir chargé — ce qui donnait l'impression d'une page qui se rafraîchit sans arrêt et faisait rater les clics, l'élément sous le pointeur étant remplacé entre l'appui et le relâchement.",
|
||||||
"Les listes déroulantes s'ouvrent par-dessus ce qui les entoure au lieu d'être coupées par lui. Celle des satellites n'affichait qu'une entrée sur huit, tronquée par le panneau qui la contenait.",
|
"Les listes déroulantes s'ouvrent par-dessus ce qui les entoure au lieu d'être coupées par lui. Celle des satellites n'affichait qu'une entrée sur huit, tronquée par le panneau qui la contenait.",
|
||||||
"Un lancement qui échoue avant l'apparition de la fenêtre dit maintenant pourquoi. OpsLog tient son journal dans le dossier data, à côté du programme : tout ce qui échouait AVANT la création de ce dossier ne laissait donc aucune trace nulle part — le processus apparaissait et disparaissait en silence. Les étapes du démarrage sont écrites dans %LOCALAPPDATA%OpsLogstartup.log, et les trois sorties silencieuses (déjà lancé, dossier data impossible à écrire — Program Files le refuse — et fenêtre qui ne s'ouvre pas, généralement un WebView2 manquant) affichent désormais un message disant laquelle c'était. Le journal indique aussi la version du runtime WebView2 installé — la seule dépendance qu'OpsLog ne peut pas embarquer, et dont l'absence est autrement invisible — et marque chaque étape jusqu'à l'affichage de la fenêtre, si bien qu'un lancement sans résultat peut être situé précisément."
|
"Un lancement qui échoue avant l'apparition de la fenêtre dit maintenant pourquoi. OpsLog tient son journal dans le dossier data, à côté du programme : tout ce qui échouait AVANT la création de ce dossier ne laissait donc aucune trace nulle part — le processus apparaissait et disparaissait en silence. Les étapes du démarrage sont écrites dans %LOCALAPPDATA%OpsLogstartup.log, et les trois sorties silencieuses (déjà lancé, dossier data impossible à écrire — Program Files le refuse — et fenêtre qui ne s'ouvre pas, généralement un WebView2 manquant) affichent désormais un message disant laquelle c'était. Le journal indique aussi la version du runtime WebView2 installé — la seule dépendance qu'OpsLog ne peut pas embarquer, et dont l'absence est autrement invisible — et marque chaque étape jusqu'à l'affichage de la fenêtre, si bien qu'un lancement sans résultat peut être situé précisément.",
|
||||||
|
"WebView2 reçoit un dossier de profil explicite sur le disque local (LOCALAPPDATAOpsLogWebView2) au lieu du dossier itinérant par défaut, qui sur un compte redirigé ou administré peut bloquer au lieu d'échouer. Et lorsqu'un lancement n'ouvre jamais sa fenêtre, le suivant réessaie sans accélération GPU et le dit — un WebView2 qui ne s'entend pas avec le pilote graphique se bloque exactement comme un WebView2 qui ne démarre pas, et c'est le seul levier qui les distingue."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
wailswindows "github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed all:frontend/dist
|
//go:embed all:frontend/dist
|
||||||
@@ -162,6 +163,8 @@ func main() {
|
|||||||
bootLog("STUCK: 12 s after wails.Run and the window has not started — WebView2 never handed control back")
|
bootLog("STUCK: 12 s after wails.Run and the window has not started — WebView2 never handed control back")
|
||||||
fatalBox("OpsLog", startupStuckMessage)
|
fatalBox("OpsLog", startupStuckMessage)
|
||||||
}()
|
}()
|
||||||
|
noteLaunchAttempt()
|
||||||
|
bootLog("WebView2 profile: %q", webviewDataPath())
|
||||||
bootLog("entering wails.Run (window %dx%d, state %v)", width, height, startState)
|
bootLog("entering wails.Run (window %dx%d, state %v)", width, height, startState)
|
||||||
err := wails.Run(&options.App{
|
err := wails.Run(&options.App{
|
||||||
Title: "OpsLog",
|
Title: "OpsLog",
|
||||||
@@ -187,6 +190,24 @@ func main() {
|
|||||||
// graphite theme's background (#16181d), which is the default on a fresh
|
// graphite theme's background (#16181d), which is the default on a fresh
|
||||||
// install — otherwise every launch flashes white first.
|
// install — otherwise every launch flashes white first.
|
||||||
BackgroundColour: &options.RGBA{R: 0x16, G: 0x18, B: 0x1d, A: 1},
|
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(),
|
||||||
|
WebviewGpuIsDisabled: lastLaunchHung,
|
||||||
|
},
|
||||||
OnStartup: app.startup,
|
OnStartup: app.startup,
|
||||||
OnDomReady: app.domReady,
|
OnDomReady: app.domReady,
|
||||||
OnBeforeClose: app.beforeClose,
|
OnBeforeClose: app.beforeClose,
|
||||||
|
|||||||
Reference in New Issue
Block a user