diff --git a/launcher/build.sh b/launcher/build.sh index 41b0ac6..5cbacc6 100644 --- a/launcher/build.sh +++ b/launcher/build.sh @@ -26,6 +26,9 @@ esac [ "$OS" = "windows" ] && OUTPUT="launch.exe" || OUTPUT="launch" -GOOS=$GOOS GOARCH=$GOARCH go build -o dist/$OUTPUT +rm -rf ./dist launch launch.exe -echo "Built: $OUTPUT" \ No newline at end of file +echo Building launcher... +GOOS=$GOOS GOARCH=$GOARCH go build -o dist/$OUTPUT || exit 1 + +echo "Complete: launcher/dist/$OUTPUT" \ No newline at end of file diff --git a/launcher/constants.go b/launcher/constants.go index 412ac7d..1b3f2c2 100644 --- a/launcher/constants.go +++ b/launcher/constants.go @@ -1,5 +1,29 @@ package main +import ( + "regexp" + "strings" +) + +// Exclude lines from output +var ExclusionRules = []string{ + // Ignore search related warnings + "^console\\.error:\\ Search", + "SearchService", + "SearchEngineSelector", + // Ignore glxtest errors + "\\[GFX1\\-\\]:", + // Ignore meaningless lines + "^console\\.error:\\ \\(\\{\\}\\)$", + "^console\\.error:\\ \"Could\\ not\\ record\\ event:\\ \"\\ \\(\\{\\}\\)$", + "^\\s*?$", + // Ignore missing urlbar provider errors + "^JavaScript\\ error:\\ resource:///modules/UrlbarProvider", +} + +// Convert ExclusionRules into a regex command +var ExclusionRegex = regexp.MustCompile(".*(" + strings.Join(ExclusionRules, "|") + ").*") + // List of fonts for each OS var FontsByOS = map[string][]string{ "windows": { diff --git a/launcher/main.go b/launcher/main.go index fb91e46..d045475 100644 --- a/launcher/main.go +++ b/launcher/main.go @@ -2,9 +2,7 @@ package main import ( "fmt" - "io" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -27,10 +25,15 @@ func main() { // Run the Camoufox executable execName := getExecutableName() + if err := setExecutablePermissions(execName); err != nil { + fmt.Printf("Error setting executable permissions: %v\n", err) + os.Exit(1) + } runCamoufox(execName, remainingArgs) } func parseArgs(args []string) (string, []string) { + // Parse the arguments var configPath string var remainingArgs []string @@ -56,6 +59,7 @@ func parseArgs(args []string) (string, []string) { } func readAndParseConfig(configInput string) map[string]interface{} { + // Unmarshal the config input into a map var configData []byte // Check if the input is a file path or inline JSON @@ -80,6 +84,7 @@ func readAndParseConfig(configInput string) map[string]interface{} { } func determineUserAgentOS(configMap map[string]interface{}) string { + // Determine the OS from the user agent string if provided defaultOS := normalizeOS(runtime.GOOS) if ua, ok := configMap["navigator.userAgent"].(string); ok { parsedUA := useragent.Parse(ua) @@ -91,6 +96,7 @@ func determineUserAgentOS(configMap map[string]interface{}) string { } func normalizeOS(osName string) string { + // Get the OS name as {macos, windows, linux} osName = strings.ToLower(osName) switch { case osName == "darwin" || strings.Contains(osName, "mac"): @@ -103,6 +109,7 @@ func normalizeOS(osName string) string { } func updateFonts(configMap map[string]interface{}, userAgentOS string) { + // Add fonts associated with the OS to the config map fonts, ok := configMap["fonts"].([]interface{}) if !ok { fonts = []interface{}{} @@ -122,6 +129,7 @@ func updateFonts(configMap map[string]interface{}, userAgentOS string) { } func setEnvironmentVariables(configMap map[string]interface{}, userAgentOS string) { + // Update the config map with the fonts and environment variables updatedConfigData, err := json.Marshal(configMap) if err != nil { fmt.Printf("Error updating config: %v\n", err) @@ -162,66 +170,3 @@ func setEnvironmentVariables(configMap map[string]interface{}, userAgentOS strin os.Setenv("FONTCONFIG_PATH", fontconfigPath) } } - -func getExecutableName() string { - switch normalizeOS(runtime.GOOS) { - case "linux": - return "./camoufox-bin" - case "macos": - return "./Camoufox.app" - case "windows": - return "./camoufox.exe" - default: - // This should never be reached due to the check in normalizeOS - return "" - } -} - -func runCamoufox(execName string, args []string) { - cmd := exec.Command(execName, args...) - - // Create pipes for stdout and stderr - stdout, err := cmd.StdoutPipe() - if err != nil { - fmt.Printf("Error creating stdout pipe: %v\n", err) - os.Exit(1) - } - stderr, err := cmd.StderrPipe() - if err != nil { - fmt.Printf("Error creating stderr pipe: %v\n", err) - os.Exit(1) - } - - // Start the command - if err := cmd.Start(); err != nil { - fmt.Printf("Error starting %s: %v\n", execName, err) - os.Exit(1) - } - - // Create a channel to signal when we're done copying output - done := make(chan bool) - - // Copy stdout and stderr to the console - go func() { - io.Copy(os.Stdout, stdout) - done <- true - }() - go func() { - io.Copy(os.Stderr, stderr) - done <- true - }() - - // Wait for both stdout and stderr to finish - <-done - <-done - - // Wait for the command to finish - if err := cmd.Wait(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - os.Exit(exitErr.ExitCode()) - } else { - fmt.Printf("Error running %s: %v\n", execName, err) - os.Exit(1) - } - } -} diff --git a/launcher/procgroup_unix.go b/launcher/procgroup_unix.go new file mode 100644 index 0000000..d85a0fc --- /dev/null +++ b/launcher/procgroup_unix.go @@ -0,0 +1,17 @@ +//go:build !windows +// +build !windows + +package main + +import ( + "os/exec" + "syscall" +) + +func setProcessGroupID(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} +} + +func killProcessGroup(cmd *exec.Cmd) { + syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) +} diff --git a/launcher/procgroup_win.go b/launcher/procgroup_win.go new file mode 100644 index 0000000..2bf68d3 --- /dev/null +++ b/launcher/procgroup_win.go @@ -0,0 +1,16 @@ +//go:build windows +// +build windows + +package main + +import ( + "os/exec" +) + +func setProcessGroupID(cmd *exec.Cmd) { + // Windows doesn't support process groups in the same way +} + +func killProcessGroup(cmd *exec.Cmd) { + cmd.Process.Kill() +} diff --git a/launcher/runner.go b/launcher/runner.go new file mode 100644 index 0000000..74735aa --- /dev/null +++ b/launcher/runner.go @@ -0,0 +1,138 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "syscall" +) + +func getExecutableName() string { + // Get the executable name based on the OS + switch normalizeOS(runtime.GOOS) { + case "linux": + return "./camoufox-bin" + case "macos": + return "./Camoufox.app" + case "windows": + return "./camoufox.exe" + default: + // This should never be reached due to the check in normalizeOS + return "" + } +} + +func setExecutablePermissions(execPath string) error { + // Set executable permissions if needed + switch normalizeOS(runtime.GOOS) { + case "macos": + return filepath.Walk(execPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + return maybeSetPermission(path, 0755) + }) + case "linux": + return maybeSetPermission(execPath, 0755) + } + return nil +} + +func maybeSetPermission(path string, mode os.FileMode) error { + info, err := os.Stat(path) + if err != nil { + return err + } + + currentMode := info.Mode().Perm() + if currentMode != mode { + return os.Chmod(path, mode) + } + return nil +} + +func filterOutput(r io.Reader, w io.Writer) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + if !ExclusionRegex.MatchString(line) { + fmt.Fprintln(w, line) + } + } +} + +func runCamoufox(execName string, args []string) { + cmd := exec.Command(execName, args...) + + setProcessGroupID(cmd) + + stdout, err := cmd.StdoutPipe() + if err != nil { + fmt.Printf("Error creating stdout pipe: %v\n", err) + os.Exit(1) + } + stderr, err := cmd.StderrPipe() + if err != nil { + fmt.Printf("Error creating stderr pipe: %v\n", err) + os.Exit(1) + } + + // Set up signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + if err := cmd.Start(); err != nil { + fmt.Printf("Error starting %s: %v\n", execName, err) + os.Exit(1) + } + + // Channel to signal when the subprocess has finished + subprocessDone := make(chan struct{}) + + // Start a goroutine to handle signals + go func() { + select { + case <-sigChan: + killProcessGroup(cmd) + case <-subprocessDone: + // Subprocess has finished, exit the Go process + os.Exit(0) + } + }() + + done := make(chan bool) + + go func() { + filterOutput(stdout, os.Stdout) + done <- true + }() + go func() { + filterOutput(stderr, os.Stderr) + done <- true + }() + + <-done + <-done + + // Wait for the command to finish + if err := cmd.Wait(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + // If the subprocess exited with an error, use its exit code + os.Exit(exitErr.ExitCode()) + } else { + fmt.Printf("Error running %s: %v\n", execName, err) + os.Exit(1) + } + } + + // Signal that the subprocess has finished + close(subprocessDone) + + // Wait here to allow the signal handling goroutine to exit the process + select {} +}