Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .codex
Empty file.
196 changes: 123 additions & 73 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,27 @@ Patch Edge Copilot - Go版本
*/
package main


import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/shirou/gopsutil/v3/process"
)

type LocalState struct {
VariationsCountry string `json:"variations_country"`
}

type Preferences struct {
Browser Browser `json:"browser"`
}

type Browser struct {
ChatIPEligibilityStatus *bool `json:"chat_ip_eligibility_status"`
}

type versionPath struct {
stable string
canary string
dev string
beta string
}

// getVersionAndUserDataPath 根据当前平台返回实际存在的 Edge 用户数据目录。
func getVersionAndUserDataPath() (map[string]string, error) {
var paths versionPath

Expand Down Expand Up @@ -99,6 +89,7 @@ func getVersionAndUserDataPath() (map[string]string, error) {
return versionDataPaths, nil
}

// shutdownEdge 关闭当前正在运行的 Edge 进程,并返回被关闭进程的可执行文件路径。
func shutdownEdge() ([]string, error) {
var terminatedEdges []string
procs, err := process.Processes()
Expand All @@ -112,23 +103,16 @@ func shutdownEdge() ([]string, error) {
continue
}

var isEdge bool
if runtime.GOOS == "darwin" {
isEdge = strings.HasPrefix(name, "Microsoft Edge")
} else {
isEdge = name == "msedge"
}

if !isEdge {
if !isEdgeProcessName(runtime.GOOS, name) {
continue
}

// Check if process is still running
// 仅处理仍在运行的主进程,避免误杀同名辅助进程。
if running, _ := p.IsRunning(); !running {
continue
}

// Skip if parent has same name (avoid killing helper processes)
// 如果父进程同名,通常说明当前是子进程或 helper,直接跳过。
if ppid, _ := p.Ppid(); ppid > 0 {
if parent, err := process.NewProcess(ppid); err == nil {
if parentName, err := parent.Name(); err == nil && parentName == name {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current heuristic parentName == name to skip helper processes works on Windows and Linux where helpers share the same executable name (msedge). However, on macOS, the main process is typically named Microsoft Edge while helpers are named Microsoft Edge Helper. This check will fail to identify helpers on macOS, causing them to be killed individually. Using isEdgeProcessName for the parent name is a more robust approach across all platforms.

Suggested change
if parentName, err := parent.Name(); err == nil && parentName == name {
if parentName, err := parent.Name(); err == nil && isEdgeProcessName(runtime.GOOS, parentName) {

Expand All @@ -150,6 +134,7 @@ func shutdownEdge() ([]string, error) {
return terminatedEdges, nil
}

// getLastVersion 读取 Edge 用户目录中的 Last Version 文件。
func getLastVersion(userDataPath string) (string, error) {
lastVersionFile := filepath.Join(userDataPath, "Last Version")
if _, err := os.Stat(lastVersionFile); os.IsNotExist(err) {
Expand All @@ -164,30 +149,21 @@ func getLastVersion(userDataPath string) (string, error) {
return strings.TrimSpace(string(data)), nil
}

// patchLocalState 仅修改 variations_country,同时保留 Local State 的其他字段。
func patchLocalState(userDataPath string) error {
localStateFile := filepath.Join(userDataPath, "Local State")
if _, err := os.Stat(localStateFile); os.IsNotExist(err) {
return fmt.Errorf("failed to patch Local State. File not found: %s", localStateFile)
}

data, err := os.ReadFile(localStateFile)
localState, err := readJSONObject(localStateFile)
if err != nil {
return err
}

var localState LocalState
if err := json.Unmarshal(data, &localState); err != nil {
return err
}

if localState.VariationsCountry != "US" {
localState.VariationsCountry = "US"
updatedData, err := json.MarshalIndent(localState, "", " ")
if err != nil {
return err
}

if err := os.WriteFile(localStateFile, updatedData, 0644); err != nil {
if localState["variations_country"] != "US" {
localState["variations_country"] = "US"
if err := writeJSONObject(localStateFile, localState); err != nil {
return err
}
fmt.Println("Succeeded in patching Local State")
Expand All @@ -198,14 +174,15 @@ func patchLocalState(userDataPath string) error {
return nil
}

// patchPreferences 只处理 Default 和 Profile * 目录下的 Preferences 文件。
func patchPreferences(userDataPath string) error {
entries, err := os.ReadDir(userDataPath)
if err != nil {
return err
}

for _, entry := range entries {
if !entry.IsDir() && entry.Name() != "Default" && !strings.HasPrefix(entry.Name(), "Profile ") {
if !entry.IsDir() || !isTargetProfileDir(entry.Name()) {
continue
}

Expand All @@ -214,29 +191,17 @@ func patchPreferences(userDataPath string) error {
continue
}

data, err := os.ReadFile(preferencesFile)
preferences, err := readJSONObject(preferencesFile)
if err != nil {
continue
}

var preferences Preferences
if err := json.Unmarshal(data, &preferences); err != nil {
continue
}

// Check if we need to patch
if preferences.Browser.ChatIPEligibilityStatus == nil || !*preferences.Browser.ChatIPEligibilityStatus {
// Set to true
trueVal := true
preferences.Browser.ChatIPEligibilityStatus = &trueVal
// browser 可能不存在,这里按需补齐,避免覆盖其他顶层字段。
browser := ensureObject(preferences, "browser")

updatedData, err := json.MarshalIndent(preferences, "", " ")
if err != nil {
fmt.Printf("Failed to marshal preferences for %s: %v\n", entry.Name(), err)
continue
}

if err := os.WriteFile(preferencesFile, updatedData, 0644); err != nil {
if browser["chat_ip_eligibility_status"] == nil || browser["chat_ip_eligibility_status"] == false {
browser["chat_ip_eligibility_status"] = true
if err := writeJSONObject(preferencesFile, preferences); err != nil {
fmt.Printf("Failed to write preferences for %s: %v\n", entry.Name(), err)
continue
}
Expand All @@ -249,27 +214,109 @@ func patchPreferences(userDataPath string) error {
return nil
}

// restartEdge 负责用已记录的可执行路径重新拉起 Edge。
func restartEdge(terminatedEdges []string) {
for _, edge := range terminatedEdges {
var cmd *string
switch runtime.GOOS {
case "windows":
cmdStr := edge + " --start-maximized"
cmd = &cmdStr
case "darwin":
cmdStr := "open -a 'Microsoft Edge' --args --start-maximized"
cmd = &cmdStr
case "linux":
cmdStr := edge + " --start-maximized"
cmd = &cmdStr
if err := restartEdges(terminatedEdges, runtime.GOOS, func(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Stderr = io.Discard
return cmd.Start()
}); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to restart Edge: %v\n", err)
}
}

// isEdgeProcessName 兼容 msedge 与 msedge.exe,并保留 macOS 的前缀匹配行为。
func isEdgeProcessName(goos string, name string) bool {
if goos == "darwin" {
return strings.HasPrefix(name, "Microsoft Edge")
}

base := strings.TrimSuffix(strings.ToLower(name), strings.ToLower(filepath.Ext(name)))
return base == "msedge"
}

// isTargetProfileDir 与 Python 版保持一致,只处理 Default 和 Profile *。
func isTargetProfileDir(name string) bool {
return name == "Default" || strings.HasPrefix(name, "Profile ")
}

// readJSONObject 读取 JSON 对象文件,便于按字段做最小修改。
func readJSONObject(path string) (map[string]any, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read JSON file %s: %w", path, err)
}

var value map[string]any
if err := json.Unmarshal(data, &value); err != nil {
return nil, fmt.Errorf("unmarshal JSON file %s: %w", path, err)
}

return value, nil
}

// writeJSONObject 将完整对象写回文件,避免像结构体精简反序列化那样丢字段。
func writeJSONObject(path string, value map[string]any) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("marshal JSON file %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("write JSON file %s: %w", path, err)
}
return nil
}
Comment on lines +259 to +268
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function has two issues:

  1. Non-atomic write: Writing directly to the target file using os.WriteFile is risky. If the process is interrupted or the disk is full during the write, the browser profile file could be corrupted. It's safer to write to a temporary file and then rename it.
  2. JSON formatting: The previous implementation used json.MarshalIndent to maintain readability. Switching to json.Marshal produces minified JSON, which makes manual inspection of browser configuration files much harder.
func writeJSONObject(path string, value map[string]any) error {
	data, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal JSON file %s: %w", path, err)
	}

	tmpPath := path + ".tmp"
	if err := os.WriteFile(tmpPath, data, 0644); err != nil {
		return fmt.Errorf("write temp JSON file %s: %w", tmpPath, err)
	}

	if err := os.Rename(tmpPath, path); err != nil {
		_ = os.Remove(tmpPath)
		return fmt.Errorf("rename JSON file %s: %w", path, err)
	}
	return nil
}


// ensureObject 保证指定键是对象;若不存在或类型不对,则创建一个空对象。
func ensureObject(parent map[string]any, key string) map[string]any {
if value, ok := parent[key].(map[string]any); ok {
return value
}

child := make(map[string]any)
parent[key] = child
return child
}

// restartEdges 会先去重,再按平台构造命令逐个重启 Edge。
func restartEdges(terminatedEdges []string, goos string, runner func(name string, args ...string) error) error {
for _, edge := range uniqueStrings(terminatedEdges) {
name, args, ok := buildRestartCommand(edge, goos)
if !ok {
continue
}

if cmd != nil {
fmt.Printf("Starting: %s\n", edge)
// Note: In a real implementation, you'd use exec.Command here
// This is simplified for the example
fmt.Printf("Starting: %s\n", edge)
if err := runner(name, args...); err != nil {
return fmt.Errorf("restart edge %s: %w", edge, err)
}
}

return nil
}

// buildRestartCommand 将“可执行文件路径 + 参数”拆开,方便测试和注入 runner。
func buildRestartCommand(edgePath, goos string) (string, []string, bool) {
switch goos {
case "windows", "linux", "darwin":
return edgePath, []string{"--start-maximized"}, true
default:
return "", nil, false
}
}

// uniqueStrings 保留原顺序去重,避免同一个 Edge 可执行文件被重复启动。
func uniqueStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}

func main() {
Expand Down Expand Up @@ -320,6 +367,9 @@ func main() {
restartEdge(terminatedEdges)
}

// 保持与原脚本一致:执行结束后等待用户确认,便于查看输出。
fmt.Print("\nPress Enter to continue...")
fmt.Scanln()
if _, err := fmt.Scanln(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to read input: %v\n", err)
}
Comment on lines +372 to +374
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

fmt.Scanln() without arguments returns an unexpected newline error when the user presses Enter (as it expects to scan at least one item). This causes the warning message to be printed every time the program exits normally. You should either ignore the error or use a different method to wait for the Enter key.

	fmt.Scanln()

}
Loading