fix: enable browser executable selection
This commit is contained in:
@@ -7,3 +7,10 @@ import "context"
|
||||
type DirectoryPicker interface {
|
||||
ChooseDirectory(context.Context) (string, error)
|
||||
}
|
||||
|
||||
// ExecutablePicker opens a platform file chooser for an existing browser
|
||||
// executable. The implementation must block only in its own goroutine; UI
|
||||
// callers receive the result asynchronously.
|
||||
type ExecutablePicker interface {
|
||||
ChooseExecutable(context.Context) (string, error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build !windows
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type unsupportedExecutablePicker struct{}
|
||||
|
||||
func NewExecutablePicker() ExecutablePicker { return unsupportedExecutablePicker{} }
|
||||
|
||||
func (unsupportedExecutablePicker) ChooseExecutable(context.Context) (string, error) {
|
||||
return "", errors.New("executable picker is only supported on Windows")
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//go:build windows
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type windowsExecutablePicker struct{ runner commandRunner }
|
||||
|
||||
func NewExecutablePicker() ExecutablePicker { return windowsExecutablePicker{runner: execRunner{}} }
|
||||
|
||||
func newExecutablePickerWithRunner(runner commandRunner) windowsExecutablePicker {
|
||||
return windowsExecutablePicker{runner: runner}
|
||||
}
|
||||
|
||||
func (p windowsExecutablePicker) ChooseExecutable(ctx context.Context) (string, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
output, err := p.runner.Run(ctx, "powershell.exe",
|
||||
"-NoProfile", "-NonInteractive", "-STA", "-WindowStyle", "Hidden", "-Command", executableDialogScript)
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return "", ctxErr
|
||||
}
|
||||
return "", fmt.Errorf("open executable picker: %w", err)
|
||||
}
|
||||
selected := strings.TrimSpace(string(output))
|
||||
if selected == "" {
|
||||
return "", ErrSelectionCanceled
|
||||
}
|
||||
if !filepath.IsAbs(selected) {
|
||||
return "", fmt.Errorf("executable picker returned a non-absolute path")
|
||||
}
|
||||
if !strings.EqualFold(filepath.Ext(selected), ".exe") {
|
||||
return "", fmt.Errorf("executable picker returned a non-executable path")
|
||||
}
|
||||
return filepath.Clean(selected), nil
|
||||
}
|
||||
|
||||
const executableDialogScript = `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$dialog.Title = '选择 Chrome 或 Edge 可执行文件'
|
||||
$dialog.Filter = '浏览器可执行文件 (*.exe)|*.exe|所有文件 (*.*)|*.*'
|
||||
$dialog.CheckFileExists = $true
|
||||
$dialog.Multiselect = $false
|
||||
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
||||
[Console]::Out.Write($dialog.FileName)
|
||||
}`
|
||||
@@ -0,0 +1,51 @@
|
||||
//go:build windows
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWindowsExecutablePickerReturnsCleanAbsoluteExecutable(t *testing.T) {
|
||||
runner := &fakeRunner{out: []byte("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\r\n")}
|
||||
picker := newExecutablePickerWithRunner(runner)
|
||||
got, err := picker.ChooseExecutable(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != `C:\Program Files\Google\Chrome\Application\chrome.exe` {
|
||||
t.Fatalf("path = %q", got)
|
||||
}
|
||||
command := strings.Join(runner.args, " ")
|
||||
if runner.name != "powershell.exe" || !strings.Contains(command, "-STA") || !strings.Contains(command, "OpenFileDialog") {
|
||||
t.Fatalf("unexpected picker command: %s %v", runner.name, runner.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsExecutablePickerRejectsInvalidSelection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
out string
|
||||
}{
|
||||
{name: "relative", out: `chrome.exe`},
|
||||
{name: "not executable", out: `C:\Browser\chrome.txt`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
picker := newExecutablePickerWithRunner(&fakeRunner{out: []byte(test.out)})
|
||||
if _, err := picker.ChooseExecutable(context.Background()); err == nil {
|
||||
t.Fatal("invalid selection was accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsExecutablePickerTreatsEmptySelectionAsCanceled(t *testing.T) {
|
||||
picker := newExecutablePickerWithRunner(&fakeRunner{})
|
||||
if _, err := picker.ChooseExecutable(context.Background()); !errors.Is(err, ErrSelectionCanceled) {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user