mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(desktop): remove the legacy remote child window
Problem:\nThe same-window Remote Workbench still carried an unreachable child-window launcher and special shell lifecycle, causing Desktop CI unused-code failures and contradicting the selected product model.\n\nRoot cause:\nThe App bindings were migrated to TargetManager, but the old ticket-based Serve HTML launch path and its window-specific startup branches were left behind.\n\nFix:\nRemove remote-window tickets, spawning, middleware, lifecycle branches, and obsolete helpers so every SSH workbench runs in the existing main window.\n\nVerification:\n- cd desktop && go test ./...\n- desktop and root golangci-lint v2.12.2: 0 issues\n- Windows amd64 Desktop cross-compilation\n- wails generate module
This commit is contained in:
1 parent
c925e14cf4
commit
b9e036b84f
9 files changed
+24
-478
No files matched your search
@@ -108,13 +108,6 @@ type PromptHistoryResult struct {
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
|
||||
// remoteWindow marks the lightweight native shell used for an SSH-backed
|
||||
// workspace. It deliberately skips local tab/session runtime startup so a
|
||||
// second window never competes for the primary process's session leases.
|
||||
remoteWindow *remoteWindowLaunch
|
||||
remoteWindowNavigated atomic.Bool
|
||||
remoteWindowOpener func(remoteWindowLaunch) error // test-only injection
|
||||
|
||||
// mu protects the tab map, tabOrder, activeTabID, and per-tab fields that are read
|
||||
// from bound methods. All bound methods that touch a controller use activeCtrl().
|
||||
mu sync.RWMutex
|
||||
@@ -454,9 +447,6 @@ func (a *App) Platform() string {
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.startWindowsWebView2StartupFallback(ctx)
|
||||
if a.remoteWindow != nil {
|
||||
return
|
||||
}
|
||||
installSystemQuitHook()
|
||||
a.startTray()
|
||||
a.enableDeferredRebuildRetry()
|
||||
@@ -491,9 +481,6 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (a *App) beforeClose(ctx context.Context) bool {
|
||||
if a.remoteWindow != nil {
|
||||
return false
|
||||
}
|
||||
if a.forceQuit.Swap(false) || consumeSystemQuitRequested() {
|
||||
return false
|
||||
}
|
||||
@@ -806,9 +793,6 @@ func (a *App) snapshotAllTabs() {
|
||||
|
||||
// shutdown snapshots all tabs, saves the final window geometry, and closes tabs.
|
||||
func (a *App) shutdown(context.Context) {
|
||||
if a.remoteWindow != nil {
|
||||
return
|
||||
}
|
||||
a.stopDeferredRebuildRetry()
|
||||
a.stopMainThreadWatchdog()
|
||||
if a.heartbeat != nil {
|
||||
@@ -877,10 +861,6 @@ func (a *App) domReady(_ context.Context) {
|
||||
// SA_ONSTACK flags required by Go; this is a no-op outside Linux.
|
||||
repairWebKitSignalHandlers()
|
||||
|
||||
if a.remoteWindow != nil {
|
||||
a.domReadyRemoteWindow()
|
||||
return
|
||||
}
|
||||
state, ok := loadWindowState()
|
||||
if ok {
|
||||
// Validate saved position against current screens. Wails v2 doesn't
|
||||
|
||||
+14
-44
@@ -104,33 +104,20 @@ func main() {
|
||||
if config.SafeModeRequested() {
|
||||
launch.SafeMode = true
|
||||
}
|
||||
var remoteLaunch *remoteWindowLaunch
|
||||
if launch.RemoteWindowTicket != "" {
|
||||
var err error
|
||||
remoteLaunch, err = consumeRemoteWindowLaunch(launch.RemoteWindowTicket)
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var tracker *repair.StartupTracker
|
||||
trackerOwned := false
|
||||
if remoteLaunch == nil {
|
||||
tracker = repair.NewStartupTracker("")
|
||||
if tracker.SafeModeRecommended() {
|
||||
launch.SafeMode = true
|
||||
}
|
||||
if launch.SafeMode {
|
||||
_ = os.Setenv("REASONIX_SAFE_MODE", "1")
|
||||
}
|
||||
// Begin runs before the Wails single-instance gate, but it refuses to
|
||||
// overwrite the recorded state while its owner PID is alive, so a duplicate
|
||||
// launch — which Wails terminates via os.Exit without OnShutdown — never
|
||||
// counts as a crash toward the Safe Mode threshold.
|
||||
startupState, _ := tracker.Begin(version, launch.SafeMode)
|
||||
trackerOwned = startupState.PID == os.Getpid()
|
||||
tracker := repair.NewStartupTracker("")
|
||||
if tracker.SafeModeRecommended() {
|
||||
launch.SafeMode = true
|
||||
}
|
||||
if launch.SafeMode {
|
||||
_ = os.Setenv("REASONIX_SAFE_MODE", "1")
|
||||
}
|
||||
// Begin runs before the Wails single-instance gate, but it refuses to
|
||||
// overwrite the recorded state while its owner PID is alive, so a duplicate
|
||||
// launch — which Wails terminates via os.Exit without OnShutdown — never
|
||||
// counts as a crash toward the Safe Mode threshold.
|
||||
startupState, _ := tracker.Begin(version, launch.SafeMode)
|
||||
trackerOwned := startupState.PID == os.Getpid()
|
||||
// Keep WebKit acceleration enabled during normal Linux launches. If the
|
||||
// startup tracker selects Safe Mode after a crash loop (or the user requests
|
||||
// it explicitly), NVIDIA systems use the broader renderer fallback before
|
||||
@@ -138,7 +125,6 @@ func main() {
|
||||
configureWebKitRendererRecovery(launch.SafeMode)
|
||||
|
||||
app := NewApp()
|
||||
app.remoteWindow = remoteLaunch
|
||||
if trackerOwned {
|
||||
app.startupTracker = tracker
|
||||
}
|
||||
@@ -147,17 +133,6 @@ func main() {
|
||||
appMenu := app.createAppMenu()
|
||||
dragAndDrop := &options.DragAndDrop{EnableFileDrop: true}
|
||||
bindings := []any{app}
|
||||
if remoteLaunch != nil {
|
||||
if remoteLaunch.Title != "" {
|
||||
title = remoteLaunch.Title
|
||||
}
|
||||
// Wails v2 has one native window per process. A remote shell must bypass
|
||||
// the primary single-instance lock and must not expose local-only menus.
|
||||
singleInstance = nil
|
||||
appMenu = nil
|
||||
dragAndDrop = &options.DragAndDrop{DisableWebViewDrop: true}
|
||||
bindings = nil
|
||||
}
|
||||
|
||||
// Restore saved window size, or fall back to the default.
|
||||
width, height := 1240, 720
|
||||
@@ -193,7 +168,6 @@ func main() {
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
Middleware: assetserver.ChainMiddleware(
|
||||
app.remoteWindowAssetMiddleware(),
|
||||
app.jsProfilingMiddleware(),
|
||||
app.remoteMarkdownImageMiddleware(),
|
||||
app.workspaceMediaMiddleware(),
|
||||
@@ -255,18 +229,14 @@ func main() {
|
||||
}
|
||||
|
||||
type desktopLaunchOptions struct {
|
||||
SafeMode bool
|
||||
RemoteWindowTicket string
|
||||
SafeMode bool
|
||||
}
|
||||
|
||||
func parseDesktopLaunchArgs(args []string) desktopLaunchOptions {
|
||||
var out desktopLaunchOptions
|
||||
for _, arg := range args {
|
||||
switch {
|
||||
case arg == "--safe-mode":
|
||||
if arg == "--safe-mode" {
|
||||
out.SafeMode = true
|
||||
case strings.HasPrefix(arg, remoteWindowTicketArgPrefix):
|
||||
out.RemoteWindowTicket = strings.TrimPrefix(arg, remoteWindowTicketArgPrefix)
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -18,16 +18,6 @@ func TestParseDesktopLaunchArgsSafeMode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDesktopLaunchArgsRemoteWindowTicket(t *testing.T) {
|
||||
got := parseDesktopLaunchArgs([]string{"--other", remoteWindowTicketArgPrefix + ".remote-window-123"})
|
||||
if got.RemoteWindowTicket != ".remote-window-123" {
|
||||
t.Fatalf("RemoteWindowTicket = %q", got.RemoteWindowTicket)
|
||||
}
|
||||
if got.SafeMode {
|
||||
t.Fatal("remote window ticket unexpectedly enabled safe mode")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMain isolates user config/state/cache dirs for the whole package. Without
|
||||
// this, tests that persist desktop state, sessions, cache, or CLI-style config
|
||||
// can leak into the developer's real Reasonix directories.
|
||||
|
||||
@@ -332,13 +332,6 @@ func ClassifyRemoteAskPassPrompt(prompt string) RemoteAskPassPromptKind {
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeRemoteAskPassPrompt strips terminal control sequences and control
|
||||
// characters before any SSH-provided text reaches the Desktop UI. Printable
|
||||
// fingerprint punctuation remains intact.
|
||||
func sanitizeRemoteAskPassPrompt(prompt string) string {
|
||||
return truncateRemoteAskPassPrompt(sanitizeRemoteAskPassPromptLimit(prompt, remoteAskPassMaxPromptBytes), 4096)
|
||||
}
|
||||
|
||||
func sanitizeRemoteAskPassPromptLimit(prompt string, maxVisibleBytes int) string {
|
||||
var builder strings.Builder
|
||||
builder.Grow(min(len(prompt), maxVisibleBytes))
|
||||
|
||||
@@ -49,5 +49,3 @@ func (p *remoteSSHProcess) kill(cmd *exec.Cmd) bool {
|
||||
})
|
||||
return killed
|
||||
}
|
||||
|
||||
func (*remoteSSHProcess) finish() {}
|
||||
@@ -1,223 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
goruntime "runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
||||
"reasonix/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
remoteWindowTicketArgPrefix = "--remote-window-ticket="
|
||||
remoteWindowTicketPrefix = ".remote-window-"
|
||||
remoteWindowTicketTTL = 2 * time.Minute
|
||||
remoteWindowTicketMaxBytes = 16 * 1024
|
||||
)
|
||||
|
||||
// remoteWindowLaunch is a one-shot handoff from the primary Reasonix process to
|
||||
// a lightweight native window process. The URL contains the local tunnel token,
|
||||
// so the descriptor lives in a mode-0600 file instead of the process arguments.
|
||||
type remoteWindowLaunch struct {
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title,omitempty"`
|
||||
}
|
||||
|
||||
func remoteWindowTicketPath(ticket string) (string, error) {
|
||||
if ticket == "" || filepath.Base(ticket) != ticket || !strings.HasPrefix(ticket, remoteWindowTicketPrefix) {
|
||||
return "", fmt.Errorf("invalid remote window ticket")
|
||||
}
|
||||
dir := strings.TrimSpace(config.MemoryUserDir())
|
||||
if dir == "" {
|
||||
return "", fmt.Errorf("cannot resolve remote window state directory")
|
||||
}
|
||||
return filepath.Join(dir, ticket), nil
|
||||
}
|
||||
|
||||
func writeRemoteWindowLaunch(launch remoteWindowLaunch) (string, error) {
|
||||
if !isSafeRemoteWindowURL(launch.URL) {
|
||||
return "", fmt.Errorf("remote window URL must use HTTP on loopback")
|
||||
}
|
||||
dir := config.MemoryUserDir()
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return "", fmt.Errorf("create remote window state directory: %w", err)
|
||||
}
|
||||
f, err := os.CreateTemp(dir, remoteWindowTicketPrefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create remote window ticket: %w", err)
|
||||
}
|
||||
path := f.Name()
|
||||
remove := true
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
if remove {
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
}()
|
||||
if err := f.Chmod(0o600); err != nil {
|
||||
return "", fmt.Errorf("secure remote window ticket: %w", err)
|
||||
}
|
||||
if err := json.NewEncoder(f).Encode(launch); err != nil {
|
||||
return "", fmt.Errorf("write remote window ticket: %w", err)
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
return "", fmt.Errorf("sync remote window ticket: %w", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return "", fmt.Errorf("close remote window ticket: %w", err)
|
||||
}
|
||||
remove = false
|
||||
return filepath.Base(path), nil
|
||||
}
|
||||
|
||||
func consumeRemoteWindowLaunch(ticket string) (*remoteWindowLaunch, error) {
|
||||
path, err := remoteWindowTicketPath(ticket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect remote window ticket: %w", err)
|
||||
}
|
||||
defer os.Remove(path)
|
||||
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return nil, fmt.Errorf("remote window ticket is not a regular file")
|
||||
}
|
||||
if info.Size() <= 0 || info.Size() > remoteWindowTicketMaxBytes {
|
||||
return nil, fmt.Errorf("remote window ticket has an invalid size")
|
||||
}
|
||||
// Windows does not expose Unix owner/group permission bits through Stat;
|
||||
// CreateTemp still creates the file for the current user, while ACLs remain
|
||||
// governed by the private user state directory.
|
||||
if goruntime.GOOS != "windows" && info.Mode().Perm()&0o077 != 0 {
|
||||
return nil, fmt.Errorf("remote window ticket permissions are too broad")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read remote window ticket: %w", err)
|
||||
}
|
||||
var launch remoteWindowLaunch
|
||||
if err := json.Unmarshal(data, &launch); err != nil {
|
||||
return nil, fmt.Errorf("decode remote window ticket: %w", err)
|
||||
}
|
||||
if !isSafeRemoteWindowURL(launch.URL) {
|
||||
return nil, fmt.Errorf("remote window URL must use HTTP on loopback")
|
||||
}
|
||||
return &launch, nil
|
||||
}
|
||||
|
||||
func isSafeRemoteWindowURL(raw string) bool {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Scheme != "http" || u.Host == "" || u.User != nil {
|
||||
return false
|
||||
}
|
||||
host := strings.TrimSpace(u.Hostname())
|
||||
if strings.EqualFold(host, "localhost") {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
func remoteWindowNavigationJS(raw string) (string, error) {
|
||||
if !isSafeRemoteWindowURL(raw) {
|
||||
return "", fmt.Errorf("remote window URL must use HTTP on loopback")
|
||||
}
|
||||
encoded, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "window.location.replace(" + string(encoded) + ");", nil
|
||||
}
|
||||
|
||||
func spawnRemoteWindow(launch remoteWindowLaunch) error {
|
||||
ticket, err := writeRemoteWindowLaunch(launch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, _ := remoteWindowTicketPath(ticket)
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
_ = os.Remove(path)
|
||||
return fmt.Errorf("locate Reasonix executable: %w", err)
|
||||
}
|
||||
cmd := exec.Command(executable, remoteWindowTicketArgPrefix+ticket)
|
||||
if err := cmd.Start(); err != nil {
|
||||
_ = os.Remove(path)
|
||||
return fmt.Errorf("start remote Reasonix window: %w", err)
|
||||
}
|
||||
// The child normally consumes the ticket immediately. This bounds any
|
||||
// leftover token file if the child exits before reaching argument parsing.
|
||||
time.AfterFunc(remoteWindowTicketTTL, func() { _ = os.Remove(path) })
|
||||
if err := cmd.Process.Release(); err != nil {
|
||||
return fmt.Errorf("release remote Reasonix window process: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func remoteWindowTitle(hostID string) string {
|
||||
hostID = strings.TrimSpace(strings.Map(func(r rune) rune {
|
||||
if unicode.IsControl(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, hostID))
|
||||
runes := []rune(hostID)
|
||||
if len(runes) > 80 {
|
||||
hostID = string(runes[:80]) + "…"
|
||||
}
|
||||
if hostID == "" {
|
||||
hostID = "Remote"
|
||||
}
|
||||
return "Reasonix [SSH: " + hostID + "]"
|
||||
}
|
||||
|
||||
func (a *App) openRemoteWindow(rawURL, hostID string) error {
|
||||
launch := remoteWindowLaunch{URL: rawURL, Title: remoteWindowTitle(hostID)}
|
||||
if a.remoteWindowOpener != nil {
|
||||
return a.remoteWindowOpener(launch)
|
||||
}
|
||||
return spawnRemoteWindow(launch)
|
||||
}
|
||||
|
||||
func (a *App) remoteWindowAssetMiddleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if a.remoteWindow == nil || (r.URL.Path != "/" && r.URL.Path != "/index.html") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
||||
_, _ = w.Write([]byte(`<!doctype html><html><head><meta charset="utf-8"><style>html{background:#1a1a2e}</style></head><body></body></html>`))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) domReadyRemoteWindow() {
|
||||
if a.remoteWindow == nil {
|
||||
return
|
||||
}
|
||||
if a.remoteWindowNavigated.CompareAndSwap(false, true) {
|
||||
if a.remoteWindow.Title != "" {
|
||||
runtime.WindowSetTitle(a.ctx, a.remoteWindow.Title)
|
||||
}
|
||||
if js, err := remoteWindowNavigationJS(a.remoteWindow.URL); err == nil {
|
||||
runtime.WindowExecJS(a.ctx, js)
|
||||
}
|
||||
}
|
||||
runtime.WindowCenter(a.ctx)
|
||||
runtime.WindowShow(a.ctx)
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/config"
|
||||
)
|
||||
|
||||
func TestRemoteWindowTicketRoundTripAndRemoval(t *testing.T) {
|
||||
launch := remoteWindowLaunch{
|
||||
URL: "http://127.0.0.1:54321/?token=secret-token",
|
||||
Title: "Reasonix [SSH: box]",
|
||||
}
|
||||
ticket, err := writeRemoteWindowLaunch(launch)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(ticket, "secret-token") || filepath.Base(ticket) != ticket {
|
||||
t.Fatalf("ticket leaked URL data or path: %q", ticket)
|
||||
}
|
||||
path, err := remoteWindowTicketPath(ticket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := info.Mode().Perm(); got != 0o600 {
|
||||
t.Fatalf("ticket permissions = %o, want 600", got)
|
||||
}
|
||||
}
|
||||
got, err := consumeRemoteWindowLaunch(ticket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *got != launch {
|
||||
t.Fatalf("launch = %+v, want %+v", *got, launch)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("ticket was not removed after consumption: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteWindowTicketRejectsUnsafeInputs(t *testing.T) {
|
||||
for _, raw := range []string{
|
||||
"https://127.0.0.1:5000/?token=x",
|
||||
"http://example.com:5000/?token=x",
|
||||
"file:///tmp/index.html",
|
||||
"javascript:alert(1)",
|
||||
} {
|
||||
if _, err := writeRemoteWindowLaunch(remoteWindowLaunch{URL: raw}); err == nil {
|
||||
t.Fatalf("unsafe URL accepted: %q", raw)
|
||||
}
|
||||
}
|
||||
for _, ticket := range []string{"", "../.remote-window-x", "/tmp/.remote-window-x", "unrelated"} {
|
||||
if _, err := remoteWindowTicketPath(ticket); err == nil {
|
||||
t.Fatalf("unsafe ticket accepted: %q", ticket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumeRemoteWindowTicketRejectsBroadPermissions(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Windows does not expose Unix permission bits through os.Stat")
|
||||
}
|
||||
dir := config.MemoryUserDir()
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ticket := remoteWindowTicketPrefix + "insecure"
|
||||
path := filepath.Join(dir, ticket)
|
||||
if err := os.WriteFile(path, []byte(`{"url":"http://127.0.0.1:5000/"}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Remove(path) })
|
||||
if _, err := consumeRemoteWindowLaunch(ticket); err == nil {
|
||||
t.Fatal("ticket with broad permissions was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsumeRemoteWindowTicketRejectsOversizedDescriptor(t *testing.T) {
|
||||
dir := config.MemoryUserDir()
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ticket := remoteWindowTicketPrefix + "oversized"
|
||||
path := filepath.Join(dir, ticket)
|
||||
if err := os.WriteFile(path, make([]byte, remoteWindowTicketMaxBytes+1), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := consumeRemoteWindowLaunch(ticket); err == nil {
|
||||
t.Fatal("oversized ticket was accepted")
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("rejected ticket was not removed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteWindowNavigationJSEscapesURL(t *testing.T) {
|
||||
js, err := remoteWindowNavigationJS("http://127.0.0.1:5000/?token=x%22);alert(1)//")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(js, "window.location.replace(\"") || !strings.HasSuffix(js, "\");") {
|
||||
t.Fatalf("unexpected navigation JS: %q", js)
|
||||
}
|
||||
if strings.Contains(js, "\");alert") {
|
||||
t.Fatalf("URL escaped the JS string: %q", js)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteWindowLifecycleSkipsPrimaryRuntime(t *testing.T) {
|
||||
a := NewApp()
|
||||
a.remoteWindow = &remoteWindowLaunch{URL: "http://127.0.0.1:5000/"}
|
||||
a.startup(context.Background())
|
||||
if a.tabsRestored != nil {
|
||||
t.Fatal("remote window initialized local tab restore")
|
||||
}
|
||||
if a.heartbeat != nil || a.tray != nil || a.remoteRuntime != nil {
|
||||
t.Fatal("remote window initialized primary-process runtime")
|
||||
}
|
||||
if a.beforeClose(context.Background()) {
|
||||
t.Fatal("remote window close was intercepted")
|
||||
}
|
||||
a.shutdown(context.Background())
|
||||
}
|
||||
|
||||
func TestRemoteWindowAssetMiddlewareDoesNotLoadPrimaryFrontend(t *testing.T) {
|
||||
a := &App{remoteWindow: &remoteWindowLaunch{URL: "http://127.0.0.1:5000/"}}
|
||||
nextCalled := false
|
||||
h := a.remoteWindowAssetMiddleware()(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
nextCalled = true
|
||||
}))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
if nextCalled {
|
||||
t.Fatal("remote shell loaded the primary asset handler")
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "<script") {
|
||||
t.Fatal("remote shell bootstrap unexpectedly contains frontend scripts")
|
||||
}
|
||||
if got := rec.Header().Get("Cache-Control"); got != "no-store" {
|
||||
t.Fatalf("Cache-Control = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteWindowTitleSanitizesHostLabel(t *testing.T) {
|
||||
if got := remoteWindowTitle(" box\nprod "); got != "Reasonix [SSH: boxprod]" {
|
||||
t.Fatalf("title = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const windowsWebView2StartupFallbackMessage = "The desktop interface did not bec
|
||||
// isolation is the primary repair; this watchdog is the last-resort visible
|
||||
// recovery path for policy-forced proxies and unrelated WebView2 failures.
|
||||
func (a *App) startWindowsWebView2StartupFallback(ctx context.Context) {
|
||||
if !shouldStartWindowsWebView2StartupFallback(goruntime.GOOS, a.remoteWindow != nil) {
|
||||
if !shouldStartWindowsWebView2StartupFallback(goruntime.GOOS) {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
@@ -47,8 +47,8 @@ func (a *App) startWindowsWebView2StartupFallback(ctx context.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
func shouldStartWindowsWebView2StartupFallback(goos string, remoteWindow bool) bool {
|
||||
return goos == "windows" && !remoteWindow
|
||||
func shouldStartWindowsWebView2StartupFallback(goos string) bool {
|
||||
return goos == "windows"
|
||||
}
|
||||
|
||||
func awaitStartupFallback(ctx context.Context, timeout <-chan time.Time, ready func() bool) bool {
|
||||
|
||||
@@ -8,19 +8,17 @@ import (
|
||||
|
||||
func TestWindowsWebView2StartupFallbackScope(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
goos string
|
||||
remoteWindow bool
|
||||
want bool
|
||||
name string
|
||||
goos string
|
||||
want bool
|
||||
}{
|
||||
{name: "primary Windows window", goos: "windows", want: true},
|
||||
{name: "remote Windows window", goos: "windows", remoteWindow: true, want: false},
|
||||
{name: "non-Windows primary window", goos: "darwin", want: false},
|
||||
{name: "Windows window", goos: "windows", want: true},
|
||||
{name: "non-Windows window", goos: "darwin", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := shouldStartWindowsWebView2StartupFallback(tt.goos, tt.remoteWindow); got != tt.want {
|
||||
t.Fatalf("shouldStartWindowsWebView2StartupFallback(%q, %v) = %v, want %v", tt.goos, tt.remoteWindow, got, tt.want)
|
||||
if got := shouldStartWindowsWebView2StartupFallback(tt.goos); got != tt.want {
|
||||
t.Fatalf("shouldStartWindowsWebView2StartupFallback(%q) = %v, want %v", tt.goos, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user