UPDATE: Enhance macOS support with new build steps, add admin relaunch functionality, and improve proxy management
Build and Release / release (push) Failing after 41s

This commit is contained in:
2026-05-24 10:18:56 +07:00
parent 1abf0caee4
commit d3ac27aa5d
10 changed files with 441 additions and 19 deletions
+33 -2
View File
@@ -20,13 +20,44 @@ jobs:
- name: Build for Windows
run: GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" .
- name: Build for macOS Intel
run: |
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 \
go build -trimpath -ldflags="-s -w" \
-o firefly-go-proxy-macos-amd64 .
- name: Build for macOS Apple Silicon
run: |
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 \
go build -trimpath -ldflags="-s -w" \
-o firefly-go-proxy-macos-arm64 .
- name: Grant execute permissions
run: |
chmod +x ./script/release-uploader
- name: Upload release
- name: Upload Windows release
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: script/release-uploader -token=$REPO_TOKEN -release-url="https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases" -files="firefly-go-proxy.exe"
run: script/release-uploader \
-token=$REPO_TOKEN \
-release-url="https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases" \
-files="firefly-go-proxy.exe"
- name: Upload macOS Intel release
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: |
script/release-uploader \
-token=$REPO_TOKEN \
-release-url="https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases" \
-files="firefly-go-proxy-macos-amd64"
- name: Upload macOS ARM release
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: |
script/release-uploader \
-token=$REPO_TOKEN \
-release-url="https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases" \
-files="firefly-go-proxy-macos-arm64"
+31
View File
@@ -7,7 +7,10 @@ import (
"fmt"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
)
func relaunchWithAdminIfNeeded() (bool, error) {
@@ -25,11 +28,17 @@ func relaunchWithAdminIfNeeded() (bool, error) {
return false, fmt.Errorf("get working directory: %w", err)
}
wrapperPID := os.Getpid()
launcherPID := os.Getppid()
args := make([]string, 0, len(os.Args))
args = append(args, shellQuote(exePath))
for _, arg := range os.Args[1:] {
args = append(args, shellQuote(arg))
}
if !hasFlagArg("parent-pid") {
args = append(args, shellQuote("-parent-pid"), shellQuote(strconv.Itoa(wrapperPID)))
}
command := fmt.Sprintf(
"cd %s && %s > /dev/null 2>&1 &",
@@ -42,5 +51,27 @@ func relaunchWithAdminIfNeeded() (bool, error) {
return false, formatCommandError("relaunch proxy as admin", err, out)
}
waitForRelaunchedProxyShutdown(launcherPID)
return true, nil
}
func waitForRelaunchedProxyShutdown(launcherPID int) {
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer signal.Stop(stop)
select {
case <-stop:
case <-parentProcessDone(launcherPID):
}
}
func hasFlagArg(name string) bool {
for _, arg := range os.Args[1:] {
trimmed := strings.TrimLeft(arg, "-")
if trimmed == name || strings.HasPrefix(trimmed, name+"=") {
return true
}
}
return false
}
+9
View File
@@ -37,6 +37,7 @@ func main() {
blockedStr := flag.String("b", "", "comma separated list of blocked ports")
proxyPort := flag.Int("p", 0, "proxy listen port (default: auto)")
exePath := flag.String("e", "", "path to the executable")
parentPID := flag.Int("parent-pid", 0, "parent process id to watch")
flag.Parse()
relaunched, err := relaunchWithAdminIfNeeded()
@@ -74,8 +75,10 @@ func main() {
proxyAddr := "127.0.0.1"
proxyEndpoint := proxyAddr + ":" + port
proxyEnabled := false
stopProxyRefresh := func() {}
defer func() {
stopProxyRefresh()
if r := recover(); r != nil {
zlog.Error().
Interface("panic", r).
@@ -93,6 +96,7 @@ func main() {
return
}
proxyEnabled = true
stopProxyRefresh = startProxyRefreshLoop(proxyAddr, port)
customCaMitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert)}
var customAlwaysMitm goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
@@ -124,6 +128,7 @@ func main() {
}
if matchDomain(host, AlwaysIgnoreDomains) {
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
return req, nil
}
@@ -167,6 +172,7 @@ func main() {
return req, nil
}
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
return req, nil
})
@@ -182,6 +188,7 @@ func main() {
stop := make(chan os.Signal, 1)
serverErr := make(chan error, 1)
parentDone := parentProcessDone(*parentPID)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
if *exePath != "" && exists(*exePath) {
go func() {
@@ -210,6 +217,8 @@ func main() {
case <-stop:
case err := <-serverErr:
zlog.Error().Err(err).Msg("ListenAndServe failed")
case <-parentDone:
zlog.Info().Int("ParentPID", *parentPID).Msg("Parent process exited")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+36
View File
@@ -0,0 +1,36 @@
//go:build darwin
// +build darwin
package main
import (
"syscall"
"time"
)
func parentProcessDone(pid int) <-chan struct{} {
if pid <= 1 {
return nil
}
done := make(chan struct{})
go func() {
defer close(done)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
if !processExists(pid) {
return
}
}
}()
return done
}
func processExists(pid int) bool {
err := syscall.Kill(pid, 0)
return err == nil || err == syscall.EPERM
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !darwin
// +build !darwin
package main
func parentProcessDone(pid int) <-chan struct{} {
return nil
}
+42
View File
@@ -0,0 +1,42 @@
//go:build darwin
// +build darwin
package main
import (
"sync"
"time"
zlog "github.com/rs/zerolog/log"
)
func startProxyRefreshLoop(host string, port string) func() {
done := make(chan struct{})
stopped := make(chan struct{})
var once sync.Once
go func() {
defer close(stopped)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
if err := setProxy(true, host, port); err != nil {
zlog.Error().Err(err).Msg("Failed to refresh macOS proxy services")
}
}
}
}()
return func() {
once.Do(func() {
close(done)
<-stopped
})
}
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !darwin
// +build !darwin
package main
func startProxyRefreshLoop(host string, port string) func() {
return func() {}
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Changelog
### UPDATE
- Go 1.26.1, latest lib
- Fix bug in macos
+2 -2
View File
@@ -1,5 +1,5 @@
{
"tag": "1.1-03",
"title": "PreBuild Version 1.1 - 03"
"tag": "1.2-01",
"title": "PreBuild Version 1.2 - 01"
}
+268 -11
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"os/exec"
"strconv"
"strings"
"sync"
)
@@ -26,6 +27,7 @@ var darwinProxyState = struct {
sync.Mutex
captured bool
previous map[string]darwinProxySettings
applied map[string]struct{}
}{}
func enabledNetworkServices() ([]string, error) {
@@ -50,6 +52,171 @@ func enabledNetworkServices() ([]string, error) {
return services, nil
}
func proxyNetworkServices() ([]string, error) {
service, defaultErr := defaultNetworkService()
if defaultErr == nil && service != "" {
return []string{service}, nil
}
services, activeErr := activeNetworkServices()
if activeErr == nil && len(services) > 0 {
return services, nil
}
if defaultErr != nil || activeErr != nil {
return nil, errors.Join(defaultErr, activeErr)
}
return nil, fmt.Errorf("no active network services found")
}
func defaultNetworkService() (string, error) {
device, err := defaultNetworkDevice()
if err != nil {
return "", err
}
servicesByDevice, err := networkServicesByDevice()
if err != nil {
return "", err
}
service := servicesByDevice[device]
if service == "" {
return "", fmt.Errorf("network service for default device %s not found", device)
}
return service, nil
}
func defaultNetworkDevice() (string, error) {
out, err := exec.Command("route", "-n", "get", "default").CombinedOutput()
if err != nil {
return "", formatCommandError("get default network route", err, out)
}
for _, line := range strings.Split(string(out), "\n") {
key, value, ok := strings.Cut(line, ":")
if ok && strings.TrimSpace(key) == "interface" {
device := strings.TrimSpace(value)
if device != "" {
return device, nil
}
}
}
return "", fmt.Errorf("default network interface not found")
}
func networkServicesByDevice() (map[string]string, error) {
out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput()
if err != nil {
return nil, formatCommandError("list network service order", err, out)
}
services := make(map[string]string)
currentService := ""
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if service, ok := parseNetworkServiceOrderName(line); ok {
currentService = service
continue
}
if currentService == "" {
continue
}
if device, ok := parseNetworkServiceOrderDevice(line); ok {
services[device] = currentService
currentService = ""
}
}
if len(services) == 0 {
return nil, fmt.Errorf("no network service devices found")
}
return services, nil
}
func parseNetworkServiceOrderName(line string) (string, bool) {
if !strings.HasPrefix(line, "(") {
return "", false
}
end := strings.Index(line, ")")
if end <= 1 {
return "", false
}
if _, err := strconv.Atoi(line[1:end]); err != nil {
return "", false
}
service := strings.TrimSpace(line[end+1:])
return service, service != ""
}
func parseNetworkServiceOrderDevice(line string) (string, bool) {
_, value, ok := strings.Cut(line, "Device:")
if !ok {
return "", false
}
value = strings.TrimSpace(value)
if idx := strings.IndexAny(value, ",)"); idx != -1 {
value = value[:idx]
}
device := strings.TrimSpace(value)
return device, device != ""
}
func activeNetworkServices() ([]string, error) {
services, err := enabledNetworkServices()
if err != nil {
return nil, err
}
active := make([]string, 0, len(services))
for _, service := range services {
ok, err := isNetworkServiceActive(service)
if err != nil {
continue
}
if ok {
active = append(active, service)
}
}
if len(active) == 0 {
return nil, fmt.Errorf("no active network services found")
}
return active, nil
}
func isNetworkServiceActive(service string) (bool, error) {
out, err := exec.Command("networksetup", "-getinfo", service).CombinedOutput()
if err != nil {
return false, formatCommandError("get network service info "+service, err, out)
}
for _, line := range strings.Split(string(out), "\n") {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
key = strings.TrimSpace(key)
if key != "IP address" && key != "IPv6 IP address" {
continue
}
value = strings.TrimSpace(value)
if value != "" && !strings.EqualFold(value, "none") {
return true, nil
}
}
return false, nil
}
func getProxySettings(service string) (darwinProxySettings, error) {
web, webErr := getProxyEndpoint("-getwebproxy", service)
secure, secureErr := getProxyEndpoint("-getsecurewebproxy", service)
@@ -166,12 +333,86 @@ func captureProxySettings(services []string) (map[string]darwinProxySettings, er
return settings, nil
}
func setProxy(enable bool, host string, port string) error {
services, err := enabledNetworkServices()
func captureMissingProxySettings(services []string) error {
missing := make([]string, 0, len(services))
for _, service := range services {
if _, ok := darwinProxyState.previous[service]; !ok {
missing = append(missing, service)
}
}
if len(missing) == 0 {
return nil
}
settings, err := captureProxySettings(missing)
if err != nil {
return err
}
for service, setting := range settings {
darwinProxyState.previous[service] = setting
}
return nil
}
func serviceSet(services []string) map[string]struct{} {
set := make(map[string]struct{}, len(services))
for _, service := range services {
set[service] = struct{}{}
}
return set
}
func appliedServicesNotIn(selected map[string]struct{}) []string {
services := make([]string, 0, len(darwinProxyState.applied))
for service := range darwinProxyState.applied {
if _, ok := selected[service]; !ok {
services = append(services, service)
}
}
return services
}
func appliedServices() []string {
services := make([]string, 0, len(darwinProxyState.applied))
for service := range darwinProxyState.applied {
services = append(services, service)
}
return services
}
func proxySettingsForServices(settings map[string]darwinProxySettings, services []string) map[string]darwinProxySettings {
filtered := make(map[string]darwinProxySettings, len(services))
for _, service := range services {
if setting, ok := settings[service]; ok {
filtered[service] = setting
}
}
return filtered
}
func resetDarwinProxyState() {
darwinProxyState.previous = nil
darwinProxyState.applied = nil
darwinProxyState.captured = false
}
func restoreAppliedProxySettings() error {
if len(darwinProxyState.applied) == 0 {
return nil
}
err := restoreProxySettings(proxySettingsForServices(
darwinProxyState.previous,
appliedServices(),
))
if err == nil {
darwinProxyState.applied = nil
}
return err
}
func setProxy(enable bool, host string, port string) error {
darwinProxyState.Lock()
defer darwinProxyState.Unlock()
@@ -180,32 +421,48 @@ func setProxy(enable bool, host string, port string) error {
return fmt.Errorf("host and port are required to enable proxy")
}
if !darwinProxyState.captured {
settings, err := captureProxySettings(services)
services, err := proxyNetworkServices()
if err != nil {
return errors.Join(err, restoreAppliedProxySettings())
}
if darwinProxyState.previous == nil {
darwinProxyState.previous = make(map[string]darwinProxySettings)
}
if err := captureMissingProxySettings(services); err != nil {
return err
}
darwinProxyState.previous = settings
darwinProxyState.captured = true
}
if err := setProxyForServices(services, host, port); err != nil {
restoreErr := restoreProxySettings(darwinProxyState.previous)
darwinProxyState.previous = nil
darwinProxyState.captured = false
resetDarwinProxyState()
return errors.Join(err, restoreErr)
}
return nil
selected := serviceSet(services)
removed := appliedServicesNotIn(selected)
restoreErr := restoreProxySettings(proxySettingsForServices(darwinProxyState.previous, removed))
if restoreErr != nil {
for _, service := range removed {
selected[service] = struct{}{}
}
}
darwinProxyState.applied = selected
return restoreErr
}
if darwinProxyState.captured {
err := restoreProxySettings(darwinProxyState.previous)
if err == nil {
darwinProxyState.previous = nil
darwinProxyState.captured = false
resetDarwinProxyState()
}
return err
}
services, err := enabledNetworkServices()
if err != nil {
return err
}
return disableProxyForServices(services)
}