mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(desktop): remove obsolete safe mode paths
Problem: v1.20 no longer enters product Safe Mode, but the desktop still carried unreachable startup recovery, theme gating, renderer fallback, and platform helper paths that kept lint red. Root cause: the initial Guard removal disconnected the entry points without deleting their dormant implementations or moving Linux-only helpers behind the Linux build tag. Fix: delete the obsolete recovery and renderer policy, simplify theme operations to the normal path, retain only ignored legacy launch tokens and always-false wire fields for upgrade compatibility, and clear the remaining Desktop lint findings. Verification: go test ./...; go vet ./...; golangci-lint run ./...; pnpm typecheck; pnpm exec tsx src/__tests__/theme-pack.test.ts; Windows amd64 cross-compile.
This commit is contained in:
1 parent
5664dc8ba7
commit
d8a4288862
23 files changed
+41
-600
No files matched your search
+1
-2
@@ -693,9 +693,8 @@ func (a *App) restoreOrBuildTabs() {
|
||||
// Run legacy config migration before the first config load so the
|
||||
// freshly written config (including the user's default_model) is
|
||||
// picked up by Load instead of falling back to built-in defaults.
|
||||
f := desktopTabsFile{}
|
||||
_, _ = config.MigrateLegacyIfNeeded()
|
||||
f = loadTabsFile()
|
||||
f := loadTabsFile()
|
||||
_, _ = recoverLegacyProjectSidebarRoots(f)
|
||||
_, _ = config.ApplyUserConfigUpgradesOnStartup(config.UserConfigPath())
|
||||
_, _ = config.MigrateMCPToUserConfigOnUpgrade(desktopMCPMigrationRoots(f))
|
||||
|
||||
@@ -2683,7 +2683,6 @@ export const en = {
|
||||
"notice.mcpServersFailed": "Some MCP servers failed to start; run /mcp for details.",
|
||||
"notice.guardianModelMissing": "Guardian was disabled because its model was not found.",
|
||||
"notice.guardianStartFailed": "Guardian was disabled because it could not start.",
|
||||
"guard.safeMode": "Reasonix started in Safe Mode. Plugins, MCP servers, hooks, bots, automations, and saved tabs are disabled for this run.",
|
||||
"questionNav.label": "Question navigation",
|
||||
"questionNav.progress": "Question {current} / {total}",
|
||||
"questionNav.jump": "Jump to question {n}",
|
||||
|
||||
@@ -1837,7 +1837,6 @@ export const zhTW: Record<DictKey, string> = {
|
||||
"notice.mcpServersFailed": "部分 MCP 服務啟動失敗,可執行 /mcp 查看詳情。",
|
||||
"notice.guardianModelMissing": "Guardian 已停用:未找到對應模型。",
|
||||
"notice.guardianStartFailed": "Guardian 啟動失敗,已停用。",
|
||||
"guard.safeMode": "Reasonix 已進入安全模式。本次執行已停用外掛、MCP、Hooks、機器人、自動化和上次儲存的分頁。",
|
||||
"questionNav.label": "問題導航",
|
||||
"questionNav.progress": "問題 {current} / {total}",
|
||||
"questionNav.jump": "跳轉到問題 {n}",
|
||||
|
||||
@@ -2686,7 +2686,6 @@ export const zh: Record<DictKey, string> = {
|
||||
"notice.mcpServersFailed": "部分 MCP 服务启动失败,可运行 /mcp 查看详情。",
|
||||
"notice.guardianModelMissing": "Guardian 已停用:未找到对应模型。",
|
||||
"notice.guardianStartFailed": "Guardian 启动失败,已停用。",
|
||||
"guard.safeMode": "Reasonix 已进入安全模式。本次运行已禁用插件、MCP、Hooks、机器人、自动化和上次保存的标签页。",
|
||||
"questionNav.label": "问题导航",
|
||||
"questionNav.progress": "问题 {current} / {total}",
|
||||
"questionNav.jump": "跳转到问题 {n}",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
func telemetryInstallProfile() string {
|
||||
return metricBucket(detectInstallProfile().Mode)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// telemetryInstallProfile returns only a bounded distribution label. Executable
|
||||
// paths are never included in telemetry or reports.
|
||||
func telemetryInstallProfile() string {
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
normalized := strings.ToLower(filepath.Clean(executable))
|
||||
if strings.Contains(normalized, `\scoop\apps\reasonix-desktop\`) ||
|
||||
strings.Contains(normalized, `\scoop\apps\reasonix\`) {
|
||||
return "scoop"
|
||||
}
|
||||
// The NSIS package always places its per-user uninstaller beside the
|
||||
// executable, including when the user selected a custom install directory.
|
||||
if info, statErr := os.Stat(filepath.Join(filepath.Dir(executable), "uninstall.exe")); statErr == nil && !info.IsDir() {
|
||||
return "installer"
|
||||
}
|
||||
if localAppData := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); localAppData != "" {
|
||||
installed := strings.ToLower(filepath.Join(localAppData, "Programs", "Reasonix"))
|
||||
if normalized == installed || strings.HasPrefix(normalized, installed+string(os.PathSeparator)) {
|
||||
return "installer"
|
||||
}
|
||||
}
|
||||
return "portable"
|
||||
}
|
||||
@@ -115,8 +115,6 @@ func main() {
|
||||
// Observe previous run for crash diagnostics only. Startup tracking must
|
||||
// never force Safe Mode, disable plugins, or select a previous binary.
|
||||
previousRun := repair.NewStartupTracker("").ObservePreviousRun()
|
||||
// Always boot the normal WebKit path.
|
||||
configureWebKitRendererRecovery(false)
|
||||
|
||||
app := NewApp()
|
||||
app.previousRun = previousRun
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"reasonix/internal/repair"
|
||||
)
|
||||
|
||||
type desktopRecoveryChoice int
|
||||
|
||||
const (
|
||||
desktopRecoveryQuit desktopRecoveryChoice = iota
|
||||
desktopRecoverySafeMode
|
||||
desktopRecoveryRepair
|
||||
)
|
||||
|
||||
type desktopStartupRecoveryDeps struct {
|
||||
recoverFailedInstall func() (repair.UpdateRollbackResult, *repair.UpdateApplyFailure, error)
|
||||
reconcilePendingUpdate func() (repair.PendingUpdateReconcileResult, error)
|
||||
rollbackPendingUpdate func() (repair.UpdateRollbackResult, error)
|
||||
repairGlobalConfig func() error
|
||||
choose func() desktopRecoveryChoice
|
||||
markClean func() error
|
||||
relaunch func(string) error
|
||||
report func(string)
|
||||
}
|
||||
|
||||
// runDesktopStartupRecovery mirrors Guard's preflight for the macOS bundle,
|
||||
// whose native executable must be the Wails process so LaunchServices, the Dock,
|
||||
// and the window all share one process identity. Windows and Linux still enter
|
||||
// through Guard and do not use this path.
|
||||
func runDesktopStartupRecovery(recommended, explicitSafeMode bool, deps desktopStartupRecoveryDeps) (safeMode, continueLaunch bool) {
|
||||
safeMode = explicitSafeMode
|
||||
report := func(message string) {
|
||||
if deps.report != nil {
|
||||
deps.report(message)
|
||||
}
|
||||
}
|
||||
relaunchRestored := func(result repair.UpdateRollbackResult) bool {
|
||||
if deps.markClean != nil {
|
||||
if err := deps.markClean(); err != nil {
|
||||
report("could not reset startup recovery state: " + err.Error())
|
||||
}
|
||||
}
|
||||
if deps.relaunch == nil {
|
||||
report("the previous Reasonix version was restored, but it could not be relaunched")
|
||||
return false
|
||||
}
|
||||
if err := deps.relaunch(result.TargetPath); err != nil {
|
||||
report("the previous Reasonix version was restored, but relaunch failed: " + err.Error())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if deps.recoverFailedInstall != nil {
|
||||
result, _, err := deps.recoverFailedInstall()
|
||||
if err != nil {
|
||||
report("update rollback after a failed install failed: " + err.Error())
|
||||
if !result.RolledBack {
|
||||
return safeMode, false
|
||||
}
|
||||
}
|
||||
if result.RolledBack {
|
||||
return safeMode, relaunchRestored(result)
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile abandoned pre-publish transactions on every launch instead of
|
||||
// waiting for a crash loop. A target release still in its health-confirmation
|
||||
// window remains probationary and is intentionally left untouched.
|
||||
if deps.reconcilePendingUpdate != nil {
|
||||
result, err := deps.reconcilePendingUpdate()
|
||||
if err != nil && !errors.Is(err, repair.ErrPendingUpdateAwaitingHealth) {
|
||||
report("previous update recovery failed: " + err.Error())
|
||||
if result.MixedInstall {
|
||||
return safeMode, false
|
||||
}
|
||||
return true, true
|
||||
}
|
||||
if result.RolledBack {
|
||||
return safeMode, relaunchRestored(repair.UpdateRollbackResult{
|
||||
RolledBack: true,
|
||||
FromVersion: result.ToVersion,
|
||||
ToVersion: result.FromVersion,
|
||||
TargetPath: result.TargetPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An explicit Safe Mode request is an operator override. Preserve Guard's
|
||||
// behavior by skipping crash-loop rollback and the recovery prompt.
|
||||
if explicitSafeMode || !recommended {
|
||||
return safeMode, true
|
||||
}
|
||||
|
||||
if deps.rollbackPendingUpdate != nil {
|
||||
result, err := deps.rollbackPendingUpdate()
|
||||
if err != nil {
|
||||
report("update rollback failed: " + err.Error())
|
||||
if result.MixedInstall {
|
||||
return safeMode, false
|
||||
}
|
||||
// Staging/verification failures leave the current release coherent.
|
||||
// Safe Mode is therefore the conservative fallback while the pending
|
||||
// transaction remains available for a later retry.
|
||||
return true, true
|
||||
}
|
||||
if result.RolledBack {
|
||||
return safeMode, relaunchRestored(result)
|
||||
}
|
||||
}
|
||||
|
||||
choice := desktopRecoverySafeMode
|
||||
if deps.choose != nil {
|
||||
choice = deps.choose()
|
||||
}
|
||||
switch choice {
|
||||
case desktopRecoveryRepair:
|
||||
if deps.repairGlobalConfig != nil {
|
||||
if err := deps.repairGlobalConfig(); err != nil {
|
||||
report("configuration repair failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
return true, true
|
||||
case desktopRecoverySafeMode:
|
||||
return true, true
|
||||
case desktopRecoveryQuit:
|
||||
return safeMode, false
|
||||
default:
|
||||
report(fmt.Sprintf("unknown recovery choice %d; starting in Safe Mode", choice))
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
//go:build darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"reasonix/internal/repair"
|
||||
)
|
||||
|
||||
func preparePackagedStartupRecovery(tracker *repair.StartupTracker, recommended, explicitSafeMode bool) (bool, bool) {
|
||||
return runDesktopStartupRecovery(recommended, explicitSafeMode, desktopStartupRecoveryDeps{
|
||||
recoverFailedInstall: repair.RecoverFailedInstall,
|
||||
reconcilePendingUpdate: func() (repair.PendingUpdateReconcileResult, error) {
|
||||
return repair.ReconcilePendingUpdate(version)
|
||||
},
|
||||
rollbackPendingUpdate: repair.RollbackPendingUpdate,
|
||||
repairGlobalConfig: func() error {
|
||||
_, err := repair.InspectAndRepairConfig(repair.ConfigOptions{Apply: true, OnlyScope: "global"})
|
||||
return err
|
||||
},
|
||||
choose: nativeDesktopRecoveryChoice,
|
||||
markClean: tracker.MarkClean,
|
||||
relaunch: func(appBundle string) error {
|
||||
if strings.TrimSpace(appBundle) == "" {
|
||||
return fmt.Errorf("restored application bundle path is empty")
|
||||
}
|
||||
return exec.Command("/usr/bin/open", "-na", appBundle).Run()
|
||||
},
|
||||
report: func(message string) {
|
||||
fmt.Fprintln(os.Stderr, "Reasonix recovery:", message)
|
||||
showDesktopRecoveryError(message)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func nativeDesktopRecoveryChoice() desktopRecoveryChoice {
|
||||
script := `button returned of (display dialog "Reasonix failed to start repeatedly. You can repair damaged global configuration or start with external integrations disabled." with title "Reasonix Recovery" buttons {"Quit", "Safe Mode", "Repair and Start"} default button "Repair and Start" with icon caution)`
|
||||
out, err := exec.Command("osascript", "-e", script).Output()
|
||||
if err != nil {
|
||||
return desktopRecoverySafeMode
|
||||
}
|
||||
switch strings.TrimSpace(string(out)) {
|
||||
case "Repair and Start":
|
||||
return desktopRecoveryRepair
|
||||
case "Safe Mode":
|
||||
return desktopRecoverySafeMode
|
||||
default:
|
||||
return desktopRecoveryQuit
|
||||
}
|
||||
}
|
||||
|
||||
func showDesktopRecoveryError(message string) {
|
||||
// Pass the message as argv rather than interpolating AppleScript so file
|
||||
// paths and command errors containing quotes cannot alter the script.
|
||||
script := `on run argv
|
||||
display dialog (item 1 of argv) with title "Reasonix Recovery" buttons {"OK"} default button "OK" with icon stop
|
||||
end run`
|
||||
_ = exec.Command("osascript", "-e", script, message).Run()
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !darwin
|
||||
|
||||
package main
|
||||
|
||||
import "reasonix/internal/repair"
|
||||
|
||||
func preparePackagedStartupRecovery(_ *repair.StartupTracker, recommended, explicitSafeMode bool) (bool, bool) {
|
||||
return explicitSafeMode || recommended, true
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/repair"
|
||||
)
|
||||
|
||||
func TestDesktopStartupRecoveryNormalLaunch(t *testing.T) {
|
||||
recovered := false
|
||||
reconciled := false
|
||||
safeMode, proceed := runDesktopStartupRecovery(false, false, desktopStartupRecoveryDeps{
|
||||
recoverFailedInstall: func() (repair.UpdateRollbackResult, *repair.UpdateApplyFailure, error) {
|
||||
recovered = true
|
||||
return repair.UpdateRollbackResult{}, nil, nil
|
||||
},
|
||||
reconcilePendingUpdate: func() (repair.PendingUpdateReconcileResult, error) {
|
||||
reconciled = true
|
||||
return repair.PendingUpdateReconcileResult{}, nil
|
||||
},
|
||||
})
|
||||
if !recovered || !reconciled || safeMode || !proceed {
|
||||
t.Fatalf("recovered=%v reconciled=%v safeMode=%v proceed=%v", recovered, reconciled, safeMode, proceed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopStartupRecoveryReconcilesAbandonedUpdateBeforeCrashThreshold(t *testing.T) {
|
||||
var relaunched string
|
||||
markedClean := false
|
||||
safeMode, proceed := runDesktopStartupRecovery(false, false, desktopStartupRecoveryDeps{
|
||||
reconcilePendingUpdate: func() (repair.PendingUpdateReconcileResult, error) {
|
||||
return repair.PendingUpdateReconcileResult{
|
||||
Pending: true,
|
||||
RolledBack: true,
|
||||
FromVersion: "v1",
|
||||
ToVersion: "v2",
|
||||
TargetPath: "/Applications/Reasonix.app",
|
||||
}, nil
|
||||
},
|
||||
markClean: func() error { markedClean = true; return nil },
|
||||
relaunch: func(path string) error { relaunched = path; return nil },
|
||||
})
|
||||
if safeMode || proceed || !markedClean || relaunched != "/Applications/Reasonix.app" {
|
||||
t.Fatalf("safeMode=%v proceed=%v markedClean=%v relaunched=%q", safeMode, proceed, markedClean, relaunched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopStartupRecoveryLeavesProbationaryUpdate(t *testing.T) {
|
||||
safeMode, proceed := runDesktopStartupRecovery(false, false, desktopStartupRecoveryDeps{
|
||||
reconcilePendingUpdate: func() (repair.PendingUpdateReconcileResult, error) {
|
||||
return repair.PendingUpdateReconcileResult{Pending: true, AwaitingHealth: true}, repair.ErrPendingUpdateAwaitingHealth
|
||||
},
|
||||
})
|
||||
if safeMode || !proceed {
|
||||
t.Fatalf("safeMode=%v proceed=%v", safeMode, proceed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopStartupRecoveryRelaunchesRestoredBundle(t *testing.T) {
|
||||
var relaunched string
|
||||
markedClean := false
|
||||
safeMode, proceed := runDesktopStartupRecovery(true, false, desktopStartupRecoveryDeps{
|
||||
recoverFailedInstall: func() (repair.UpdateRollbackResult, *repair.UpdateApplyFailure, error) {
|
||||
return repair.UpdateRollbackResult{}, nil, nil
|
||||
},
|
||||
rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) {
|
||||
return repair.UpdateRollbackResult{RolledBack: true, TargetPath: "/Applications/Reasonix.app"}, nil
|
||||
},
|
||||
markClean: func() error { markedClean = true; return nil },
|
||||
relaunch: func(path string) error { relaunched = path; return nil },
|
||||
})
|
||||
if safeMode || proceed || !markedClean || relaunched != "/Applications/Reasonix.app" {
|
||||
t.Fatalf("safeMode=%v proceed=%v markedClean=%v relaunched=%q", safeMode, proceed, markedClean, relaunched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopStartupRecoveryFailedInstallFailsClosed(t *testing.T) {
|
||||
var reports []string
|
||||
_, proceed := runDesktopStartupRecovery(false, false, desktopStartupRecoveryDeps{
|
||||
recoverFailedInstall: func() (repair.UpdateRollbackResult, *repair.UpdateApplyFailure, error) {
|
||||
return repair.UpdateRollbackResult{}, &repair.UpdateApplyFailure{}, errors.New("restore failed")
|
||||
},
|
||||
report: func(message string) { reports = append(reports, message) },
|
||||
})
|
||||
if proceed {
|
||||
t.Fatal("desktop proceeded after an incomplete failed-install rollback")
|
||||
}
|
||||
if len(reports) != 1 || !strings.Contains(reports[0], "restore failed") {
|
||||
t.Fatalf("reports = %#v", reports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopStartupRecoveryCoherentRollbackFailureUsesSafeMode(t *testing.T) {
|
||||
safeMode, proceed := runDesktopStartupRecovery(true, false, desktopStartupRecoveryDeps{
|
||||
rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) {
|
||||
return repair.UpdateRollbackResult{}, errors.New("backup unavailable")
|
||||
},
|
||||
})
|
||||
if !safeMode || !proceed {
|
||||
t.Fatalf("safeMode=%v proceed=%v, want true true", safeMode, proceed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopStartupRecoveryMixedInstallFailsClosed(t *testing.T) {
|
||||
safeMode, proceed := runDesktopStartupRecovery(true, false, desktopStartupRecoveryDeps{
|
||||
rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) {
|
||||
return repair.UpdateRollbackResult{MixedInstall: true}, errors.New("compensation failed")
|
||||
},
|
||||
})
|
||||
if safeMode || proceed {
|
||||
t.Fatalf("safeMode=%v proceed=%v, want false false", safeMode, proceed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopStartupRecoveryRepairStartsSafeMode(t *testing.T) {
|
||||
repaired := false
|
||||
safeMode, proceed := runDesktopStartupRecovery(true, false, desktopStartupRecoveryDeps{
|
||||
rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) {
|
||||
return repair.UpdateRollbackResult{}, nil
|
||||
},
|
||||
choose: func() desktopRecoveryChoice { return desktopRecoveryRepair },
|
||||
repairGlobalConfig: func() error {
|
||||
repaired = true
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if !repaired || !safeMode || !proceed {
|
||||
t.Fatalf("repaired=%v safeMode=%v proceed=%v, want true true true", repaired, safeMode, proceed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDesktopStartupRecoveryExplicitSafeModeSkipsCrashRollback(t *testing.T) {
|
||||
rollbackCalled := false
|
||||
safeMode, proceed := runDesktopStartupRecovery(true, true, desktopStartupRecoveryDeps{
|
||||
rollbackPendingUpdate: func() (repair.UpdateRollbackResult, error) {
|
||||
rollbackCalled = true
|
||||
return repair.UpdateRollbackResult{}, nil
|
||||
},
|
||||
})
|
||||
if rollbackCalled || !safeMode || !proceed {
|
||||
t.Fatalf("rollbackCalled=%v safeMode=%v proceed=%v", rollbackCalled, safeMode, proceed)
|
||||
}
|
||||
}
|
||||
+3
-30
@@ -68,17 +68,13 @@ func (a *App) ListThemePacks() ([]ThemePackView, error) {
|
||||
activeID := resolveActiveThemeID(st)
|
||||
baseStyle := a.desktopBaseStyleLocked()
|
||||
|
||||
safe := a.themeSafeMode()
|
||||
var out []ThemePackView
|
||||
for _, m := range builtinThemePacks() {
|
||||
cp := m
|
||||
// Base "active" = no pack applied and this is the configured base style.
|
||||
baseActive := !safe && activeID == "" && baseStyle == m.ID
|
||||
baseActive := activeID == "" && baseStyle == m.ID
|
||||
out = append(out, manifestToView(&cp, themeKindBase, baseActive, "", ""))
|
||||
}
|
||||
if safe {
|
||||
return out, nil
|
||||
}
|
||||
for _, ot := range officialThemes() {
|
||||
m := ot.manifest
|
||||
bgURL := officialAssetURL(m.ID, m.Background.Image)
|
||||
@@ -107,18 +103,13 @@ func (a *App) ListThemePacks() ([]ThemePackView, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetActiveThemePack returns the currently enabled pack (nil pack when none / safe mode).
|
||||
// Safe mode suppresses pack application but does not delete the stored id.
|
||||
// GetActiveThemePack returns the currently enabled pack (nil pack when none).
|
||||
func (a *App) GetActiveThemePack() (ThemeActiveView, error) {
|
||||
themeMu.Lock()
|
||||
defer themeMu.Unlock()
|
||||
|
||||
view := ThemeActiveView{SafeMode: a.themeSafeMode()}
|
||||
view := ThemeActiveView{}
|
||||
st := a.migrateThemeDesktopStateLocked()
|
||||
if view.SafeMode {
|
||||
// Do not clear ActiveThemeID — safe mode only blocks loading.
|
||||
return view, nil
|
||||
}
|
||||
activeID := resolveActiveThemeID(st)
|
||||
if st.ActiveThemeID != "" && activeID == "" {
|
||||
// Broken or migrated-away pointer: clear so the next launch is clean.
|
||||
@@ -146,7 +137,6 @@ func (a *App) GetThemeExperience() (ThemeExperienceView, error) {
|
||||
themeMu.Lock()
|
||||
defer themeMu.Unlock()
|
||||
|
||||
safe := a.themeSafeMode()
|
||||
// Migrate first so a v1 base-style activeThemeId lands in desktop.theme_style
|
||||
// before we read appearance.
|
||||
st := a.migrateThemeDesktopStateLocked()
|
||||
@@ -155,10 +145,6 @@ func (a *App) GetThemeExperience() (ThemeExperienceView, error) {
|
||||
ThemeMode: themeMode,
|
||||
BaseStyle: baseStyle,
|
||||
EffectiveStyle: baseStyle,
|
||||
SafeMode: safe,
|
||||
}
|
||||
if safe {
|
||||
return view, nil
|
||||
}
|
||||
activeID := resolveActiveThemeID(st)
|
||||
if st.ActiveThemeID != "" && activeID == "" {
|
||||
@@ -219,9 +205,6 @@ func (a *App) ActivateThemePack(id string) error {
|
||||
defer themeMu.Unlock()
|
||||
|
||||
id = strings.TrimSpace(id)
|
||||
if a.themeSafeMode() && id != "" {
|
||||
return fmt.Errorf("safe mode does not load external themes")
|
||||
}
|
||||
st := a.migrateThemeDesktopStateLocked()
|
||||
if id == "" {
|
||||
st.ActiveThemeID = ""
|
||||
@@ -357,9 +340,6 @@ func (a *App) SaveThemePack(input ThemeSaveInput) (ThemePackView, error) {
|
||||
themeMu.Lock()
|
||||
defer themeMu.Unlock()
|
||||
|
||||
if a.themeSafeMode() {
|
||||
return ThemePackView{}, fmt.Errorf("safe mode cannot save themes")
|
||||
}
|
||||
m := &ThemePackManifest{
|
||||
SchemaVersion: themePackSchemaVersion,
|
||||
ID: strings.TrimSpace(input.ID),
|
||||
@@ -520,9 +500,6 @@ func (a *App) CopyThemePack(sourceID, newID, newName string) (ThemePackView, err
|
||||
themeMu.Lock()
|
||||
defer themeMu.Unlock()
|
||||
|
||||
if a.themeSafeMode() {
|
||||
return ThemePackView{}, fmt.Errorf("safe mode cannot copy themes")
|
||||
}
|
||||
sourceID = strings.TrimSpace(sourceID)
|
||||
newID = strings.TrimSpace(newID)
|
||||
if !themePackIDRe.MatchString(newID) || isReservedThemeID(newID) {
|
||||
@@ -607,10 +584,6 @@ func (a *App) ImportThemePack(sourcePath string, replace bool) (ThemeImportResul
|
||||
themeMu.Lock()
|
||||
defer themeMu.Unlock()
|
||||
|
||||
if a.themeSafeMode() {
|
||||
return ThemeImportResult{}, fmt.Errorf("safe mode cannot import themes")
|
||||
}
|
||||
|
||||
// Confirm a previously staged conflict without re-picking a file.
|
||||
path := strings.TrimSpace(sourcePath)
|
||||
if path == "" && replace {
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/config"
|
||||
)
|
||||
|
||||
const themeAssetURLPrefix = "/__reasonix_theme_asset/"
|
||||
@@ -31,11 +29,6 @@ func (a *App) themeAssetMiddleware() func(http.Handler) http.Handler {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
// Safe mode never serves external theme assets.
|
||||
if a.themeSafeMode() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
rest := strings.TrimPrefix(r.URL.Path, themeAssetURLPrefix)
|
||||
parts := strings.Split(rest, "/")
|
||||
if len(parts) != 3 {
|
||||
@@ -145,12 +138,6 @@ func serveOfficialThemeAsset(w http.ResponseWriter, r *http.Request, themeID, di
|
||||
http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(data))
|
||||
}
|
||||
|
||||
func (a *App) themeSafeMode() bool {
|
||||
// DesktopStartupSettings exposes SafeMode; keep a local helper so asset
|
||||
// middleware and list/load paths share the same gate.
|
||||
return config.SafeModeRequested()
|
||||
}
|
||||
|
||||
func themeBackgroundURL(themeID, imageName string) string {
|
||||
if imageName == "" || isBuiltinThemeID(themeID) {
|
||||
return ""
|
||||
|
||||
@@ -166,7 +166,8 @@ type ThemeContrastWarning struct {
|
||||
type ThemeActiveView struct {
|
||||
ActiveThemeID string `json:"activeThemeId,omitempty"`
|
||||
Pack *ThemePackView `json:"pack,omitempty"`
|
||||
SafeMode bool `json:"safeMode"`
|
||||
// SafeMode is retained as an always-false compatibility field for older frontends.
|
||||
SafeMode bool `json:"safeMode"`
|
||||
}
|
||||
|
||||
// ThemeExperienceView is the unified appearance state for the redesigned
|
||||
@@ -178,7 +179,8 @@ type ThemeExperienceView struct {
|
||||
EffectiveStyle string `json:"effectiveStyle"` // pack.baseStyle when pack active, else baseStyle
|
||||
ActiveThemeID string `json:"activeThemeId,omitempty"` // official/user only; never a base id
|
||||
ActivePack *ThemePackView `json:"activePack,omitempty"`
|
||||
SafeMode bool `json:"safeMode"`
|
||||
// SafeMode is retained as an always-false compatibility field for older frontends.
|
||||
SafeMode bool `json:"safeMode"`
|
||||
}
|
||||
|
||||
// ThemeSaveInput is the editor payload for creating/updating a user theme.
|
||||
|
||||
@@ -5,11 +5,7 @@ package main
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
errUpdateAuthCancelled = fmt.Errorf("update: authorization cancelled")
|
||||
errUpdateAuthFailed = fmt.Errorf("update: authorization failed")
|
||||
errUpdatePkgBusy = fmt.Errorf("update: package manager busy")
|
||||
errUpdatePkgInstall = fmt.Errorf("update: package install failed")
|
||||
errUpdatePkgVerify = fmt.Errorf("update: package verify failed")
|
||||
errUpdateCacheMismatch = fmt.Errorf("update: cached artifact does not match current install mode")
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -126,25 +125,6 @@ func selectUpdateAsset(m *update.Manifest, profile installProfile) (update.Asset
|
||||
return update.Asset{}, "", false
|
||||
}
|
||||
|
||||
// dirIsWritable reports whether the process can create a temporary file in dir.
|
||||
func dirIsWritable(dir string) bool {
|
||||
if dir == "" {
|
||||
return false
|
||||
}
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return false
|
||||
}
|
||||
f, err := os.CreateTemp(dir, ".reasonix-write-test-*")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
name := f.Name()
|
||||
_ = f.Close()
|
||||
_ = os.Remove(name)
|
||||
return true
|
||||
}
|
||||
|
||||
func linuxDebHelperReady() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
@@ -158,18 +138,6 @@ func linuxDebHelperReady() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// resolveExecutablePath returns the real path of the running binary.
|
||||
func resolveExecutablePath() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
exe = resolved
|
||||
}
|
||||
return exe
|
||||
}
|
||||
|
||||
func manualDebInstallHint() string {
|
||||
return "Install manually with: sudo apt install ./Reasonix-linux-amd64.deb"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,37 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// dirIsWritable reports whether the process can create a temporary file in dir.
|
||||
func dirIsWritable(dir string) bool {
|
||||
if dir == "" {
|
||||
return false
|
||||
}
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return false
|
||||
}
|
||||
f, err := os.CreateTemp(dir, ".reasonix-write-test-*")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
name := f.Name()
|
||||
_ = f.Close()
|
||||
_ = os.Remove(name)
|
||||
return true
|
||||
}
|
||||
|
||||
// resolveExecutablePath returns the real path of the running binary.
|
||||
func resolveExecutablePath() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
exe = resolved
|
||||
}
|
||||
return exe
|
||||
}
|
||||
|
||||
// detectLinuxInstallProfile classifies the running Linux install.
|
||||
//
|
||||
// deb — dpkg owns the absolute executable path as reasonix-desktop, and the
|
||||
|
||||
@@ -108,18 +108,6 @@ import "C"
|
||||
// the verified JavaScriptCore signal-handler race, but is not intended to cover
|
||||
// failures that occur earlier while WebKit constructs the window.
|
||||
|
||||
// configureWebKitRendererRecovery applies WebKit's costly DMA-BUF fallback only
|
||||
// during Safe Mode on NVIDIA systems. Normal launches keep accelerated backing
|
||||
// store enabled; NVIDIA Wayland sessions continue to use the narrower explicit-
|
||||
// sync workaround in nvidia_wayland_linux.go. Call this before wails.Run so the
|
||||
// WebKit process inherits the environment.
|
||||
func configureWebKitRendererRecovery(safeMode bool) {
|
||||
if !safeMode {
|
||||
return
|
||||
}
|
||||
configureWebKitRendererRecoveryForGPU(safeMode, hasNVIDIAGPU())
|
||||
}
|
||||
|
||||
// scheduleWebKitSignalHandlerRepair starts the bounded GLib timer immediately
|
||||
// before wails.Run. The callback begins executing when Wails enters GTK's main
|
||||
// loop, which anchors the five-second repair window to WebKit startup instead
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
package main
|
||||
|
||||
func configureWebKitRendererRecovery(bool) {}
|
||||
|
||||
func scheduleWebKitSignalHandlerRepair() {}
|
||||
|
||||
func repairWebKitSignalHandlers() {}
|
||||
@@ -1,28 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigureWebKitRendererRecoveryIsNoop(t *testing.T) {
|
||||
const key = "WEBKIT_DISABLE_DMABUF_RENDERER"
|
||||
previous, set := os.LookupEnv(key)
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if set {
|
||||
_ = os.Setenv(key, previous)
|
||||
} else {
|
||||
_ = os.Unsetenv(key)
|
||||
}
|
||||
})
|
||||
|
||||
configureWebKitRendererRecovery(true)
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
t.Fatalf("non-Linux recovery unexpectedly set %s=%q", key, value)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
// configureWebKitRendererRecoveryForGPU keeps the environment policy separate
|
||||
// from Linux GPU detection so every platform can regression-test the decision.
|
||||
// Only the Linux wrapper invokes it in production.
|
||||
func configureWebKitRendererRecoveryForGPU(safeMode, nvidiaGPU bool) {
|
||||
if !safeMode || !nvidiaGPU {
|
||||
return
|
||||
}
|
||||
if _, explicitlySet := os.LookupEnv("WEBKIT_DISABLE_DMABUF_RENDERER"); explicitlySet {
|
||||
return
|
||||
}
|
||||
_ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigureWebKitRendererRecoveryForGPU(t *testing.T) {
|
||||
const key = "WEBKIT_DISABLE_DMABUF_RENDERER"
|
||||
tests := []struct {
|
||||
name string
|
||||
safeMode bool
|
||||
nvidiaGPU bool
|
||||
explicit *string
|
||||
wantSet bool
|
||||
wantValue string
|
||||
}{
|
||||
{name: "normal NVIDIA launch keeps acceleration", nvidiaGPU: true},
|
||||
{name: "safe mode without NVIDIA keeps acceleration", safeMode: true},
|
||||
{name: "safe mode NVIDIA launch disables DMA-BUF", safeMode: true, nvidiaGPU: true, wantSet: true, wantValue: "1"},
|
||||
{name: "preconfigured zero value is preserved", safeMode: true, nvidiaGPU: true, explicit: stringPointer("0"), wantSet: true, wantValue: "0"},
|
||||
{name: "preconfigured enabled value is preserved", safeMode: true, nvidiaGPU: true, explicit: stringPointer("1"), wantSet: true, wantValue: "1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
restoreEnv(t, key)
|
||||
if tt.explicit != nil {
|
||||
if err := os.Setenv(key, *tt.explicit); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
configureWebKitRendererRecoveryForGPU(tt.safeMode, tt.nvidiaGPU)
|
||||
|
||||
got, set := os.LookupEnv(key)
|
||||
if set != tt.wantSet || got != tt.wantValue {
|
||||
t.Fatalf("%s = %q, set=%v; want %q, set=%v", key, got, set, tt.wantValue, tt.wantSet)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stringPointer(value string) *string { return &value }
|
||||
|
||||
func restoreEnv(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
previous, set := os.LookupEnv(key)
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if set {
|
||||
_ = os.Setenv(key, previous)
|
||||
} else {
|
||||
_ = os.Unsetenv(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -12,9 +12,9 @@ const windowsWebView2StartupFallbackDelay = 15 * time.Second
|
||||
|
||||
const windowsWebView2StartupFallbackMessage = "The desktop interface did not become ready within 15 seconds. " +
|
||||
"An unavailable Windows system proxy or a WebView2 failure may be blocking startup. " +
|
||||
"Check the system proxy, then restart Reasonix. If the problem continues, start Reasonix with --safe-mode.\n\n" +
|
||||
"Check the system proxy, then restart Reasonix. If the problem continues, repair Microsoft Edge WebView2 Runtime or reinstall Reasonix.\n\n" +
|
||||
"桌面界面在 15 秒内未能就绪。不可用的 Windows 系统代理或 WebView2 故障可能阻塞了启动。" +
|
||||
"请检查系统代理后重启 Reasonix;如果问题仍然存在,请使用 --safe-mode 启动。"
|
||||
"请检查系统代理后重启 Reasonix;如果问题仍然存在,请修复 Microsoft Edge WebView2 Runtime 或重新安装 Reasonix。"
|
||||
|
||||
// startWindowsWebView2StartupFallback prevents StartHidden from turning a slow
|
||||
// or failed WebView2 navigation into an apparently missing application. Proxy
|
||||
|
||||
Reference in new issue
Block a user