Skip to content
Merged
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
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '^1.24.2'
go-version: 'stable'

- name: Update version in main.go to ${{ github.event.release.tag_name }}
run: |
# Use sed to update the currentVersion variable in main.go
sed -i 's/var currentVersion = ".*"/var currentVersion = "${{ github.event.release.tag_name }}"/' ./main.go

- name: Commit main.go with updated version
Expand All @@ -33,8 +32,9 @@ jobs:
git commit -m "Updated ./main.go"
git push origin HEAD:${{ github.event.release.target_commitish }}

- name: Install rsrc (for embedding icon)
run: go install github.com/akavel/rsrc@latest
- name: Install rsrc to embed icon into application
run: |
go install github.com/akavel/rsrc@latest

- name: Generate Windows resource file with icon
run: |
Expand All @@ -44,7 +44,7 @@ jobs:
run: |
GOOS=windows GOARCH=amd64 go build -ldflags="-H windowsgui" -o AutoExitNode.exe

- name: Upload AutoExitNode.exe to release
- name: Upload AutoExitNode.exe to the release
uses: svenstaro/upload-release-action@2.11.1
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# AutoExitNode

[![GitHub Release][releases-shield]][releases]
[![GitHub Downloads][downloads-shield]][downloads]
[![License][license-shield]][license]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]

![Icon](icon_active.png)

**AutoExitNode** is a Windows system tray application that automatically manages your Tailscale exit node based on your network (WiFi SSID or cellular connection).

## Features
Expand Down Expand Up @@ -49,14 +56,6 @@
- Check for update (checks GitHub for new version)
- Quit (exit the app)

## Development & Testing

- Run `go build` to build the program.
- Unit tests are in `main_test.go`:
```
go test
```

## Requirements

- Windows 10 or newer
Expand All @@ -76,3 +75,12 @@ The app automatically checks for new versions on GitHub and shows a Windows noti

**Note:**
This project is not officially affiliated with Tailscale.

[releases-shield]: https://img.shields.io/github/v/release/woopstar/AutoExitNode?style=for-the-badge
[releases]: https://github.com/woopstar/AutoExitNode/releases
[downloads-shield]: https://img.shields.io/github/downloads/woopstar/AutoExitNode/total.svg?style=for-the-badge
[downloads]: https://github.com/woopstar/AutoExitNode/releases
[license-shield]: https://img.shields.io/github/license/woopstar/AutoExitNode?style=for-the-badge
[license]: https://github.com/woopstar/AutoExitNode/blob/main/LICENSE
[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-FFDD00.svg?style=for-the-badge&logo=buymeacoffee
[buymecoffee]: https://www.buymeacoffee.com/woopstar
73 changes: 61 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"github.com/getlantern/systray"
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
"github.com/go-toast/toast"
)

//go:embed icon_active.ico
Expand All @@ -41,6 +40,9 @@ var tailscaleAvailable = true

var currentVersion = "v1.2.1" // Default version, will be overwritten by config if present

var latestVersion string
var latestVersionURL string

func main() {
loadConfig()
tailscaleAvailable = checkTailscaleExists()
Expand Down Expand Up @@ -102,7 +104,11 @@ func onReady() {
mRunAtStartup.Check()
}
case <-mCheckUpdate.ClickedCh:
go checkForUpdate()
go func() {
checkForUpdate(func(ver, url string) {
updateVersionMenu(mVersion, ver, url)
})
}()
case <-mQuit.ClickedCh:
systray.Quit()
return
Expand All @@ -117,8 +123,31 @@ func onReady() {
}
}()

// Automatic update check at startup (can be removed if not desired)
go checkForUpdate()
// Periodically check for updates in the background
go func() {
for {
checkForUpdate(func(ver, url string) {
updateVersionMenu(mVersion, ver, url)
})
time.Sleep(15 * time.Minute)
}
}()

// Initial update check at startup
go checkForUpdate(func(ver, url string) {
updateVersionMenu(mVersion, ver, url)
})
}

// Update the version menu item if a new version is available
func updateVersionMenu(mVersion *systray.MenuItem, ver, url string) {
if ver != "" && ver != currentVersion {
mVersion.SetTitle(fmt.Sprintf("Version: %s (Update: %s)", currentVersion, ver))
mVersion.SetTooltip(fmt.Sprintf("New version available: %s\n%s", ver, url))
} else {
mVersion.SetTitle(fmt.Sprintf("Version: %s", currentVersion))
mVersion.SetTooltip("Current version")
}
}

func checkAndApply(mStatus *systray.MenuItem) {
Expand Down Expand Up @@ -317,41 +346,61 @@ func removeStartupShortcut() {
}
}

func checkForUpdate() {
const repo = "woopstar/AutoExitNode" // Set to your repo
func checkForUpdate(cb func(version, url string)) {
const repo = "woopstar/AutoExitNode" // Ensure this matches your GitHub repo (owner/repo)
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("checkForUpdate: failed to create request:", err)
return
}
req.Header.Set("Accept", "application/vnd.github+json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Println("checkForUpdate: HTTP request failed:", err)
return
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
fmt.Printf("checkForUpdate: unexpected status code: %d\n", resp.StatusCode)
return
}

var data struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
fmt.Println("checkForUpdate: failed to decode JSON:", err)
return
}

if data.TagName != "" && data.TagName != currentVersion {
latestVersion = data.TagName
latestVersionURL = data.HTMLURL
showWindowsNotification("Update available!", fmt.Sprintf("New version: %s\nSee: %s", data.TagName, data.HTMLURL))
if cb != nil {
cb(data.TagName, data.HTMLURL)
}
} else if cb != nil {
cb("", "")
}
}

// showWindowsNotification displays a notification on Windows using go-toast.
func showWindowsNotification(title, message string) {
(&toast.Notification{
AppID: "AutoExitNode",
Title: title,
Message: message,
Icon: "icon_active.ico",
}).Push()
// Show a simple popup using Windows MessageBox via PowerShell for maximum compatibility.
cmd := exec.Command("powershell", "-Command", fmt.Sprintf(`Add-Type -AssemblyName PresentationFramework;[System.Windows.MessageBox]::Show('%s', '%s')`, escapeForPowerShell(message), escapeForPowerShell(title)))
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
if err := cmd.Run(); err != nil {
fmt.Println("showWindowsNotification: failed to show popup:", err)
}
}

// escapeForPowerShell escapes single quotes for PowerShell string literals.
func escapeForPowerShell(s string) string {
return strings.ReplaceAll(s, "'", "''")
}
Loading