Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
146a709358 | ||
|
|
5f98c2834f | ||
|
|
baff9449bd |
@@ -189,14 +189,24 @@ what we take vs. skip). Never vendor it into the build.
|
||||
- No third-party deps beyond `github.com/coder/websocket`,
|
||||
`golang.org/x/crypto` (ACME), and `fyne.io/fyne/v2` (desktop UI). Keep it
|
||||
that way unless a milestone calls for it.
|
||||
- **CLI release**: `v0.1.0` (tag on `ebae420`) is published on Gitea with
|
||||
darwin/linux arm64+amd64 tarballs and `checksums.txt`. The Homebrew formula
|
||||
is `tzgyn-tunnel` in `TZGyn/homebrew-tap` (it installs the `tunnel` binary;
|
||||
the name avoids the deprecated core `tunnel` formula). Rebuild with
|
||||
`-X main.version=<version>`, attach all four assets, and update the four
|
||||
URLs/SHAs in the formula on each release. `tunnel up` currently starts a
|
||||
detached process, not a launchd agent (the official CLI's agent was stopped
|
||||
when the live tunnel was switched to this client).
|
||||
- **Releases**: `v0.2.1` (tag on `5f98c28`) is published on Gitea with
|
||||
darwin/linux CLI tarballs and a **notarized** `Tunnel-0.2.1.zip`
|
||||
(`spctl` = accepted / `Notarized Developer ID`). Homebrew: formula
|
||||
`tzgyn-tunnel` (CLI, installs `tunnel`) and cask `tzgyn-tunnel-app` (desktop,
|
||||
installs `Tunnel.app`), both in `TZGyn/homebrew-tap`. Rebuild the app with
|
||||
`scripts/build-desktop-app.sh <version>` for a local ad-hoc build, or
|
||||
`scripts/release-desktop-app.sh <version>` for the signed+notarized zip;
|
||||
update both tap files' URLs/SHAs on each release. The app has
|
||||
**Connect on launch** (`auto_connect`); add it to macOS Login Items to
|
||||
reconnect at login. The CLI `tunnel up` remains a detached process; the
|
||||
desktop app runs the service in-process. `tunnel check [route]` and the
|
||||
desktop **Check now** button check the local TCP target plus a public TLS
|
||||
handshake (DNS, proxy, bridge, certificate); public checks use a 5s deadline.
|
||||
- **macOS is case-insensitive**: `.local/tunnel` (CLI) and `.local/Tunnel`
|
||||
(desktop) are the same file. The desktop scripts build to
|
||||
`.local/desktop/Tunnel`; use `.local/tunnel-cli` when testing both
|
||||
side-by-side. Symptom of a collision: `tunnel check` opens a GUI window and
|
||||
`check` hangs/does nothing.
|
||||
- Milestones: M1 core data path (done); M2 real ACME DNS-01 + renewals (done);
|
||||
M3 rate limits/admin/hardening (done); M4 Go client (done); M5 desktop app
|
||||
(done). Remaining optional work: launchd/systemd agents for the Go client,
|
||||
|
||||
@@ -69,6 +69,7 @@ Routes reload without restarting the service.
|
||||
| `tunnel config show` | show API, tunnel, certificate and routes (never the token) |
|
||||
| `tunnel route add <port\|host:port> [--name NAME] [--proxy-protocol v1\|v2]` | add/update a route; random name if omitted |
|
||||
| `tunnel route remove <name>` / `route list` | manage routes |
|
||||
| `tunnel check [name]` | check local TCP targets and the public TLS path |
|
||||
| `tunnel up` / `down` | start/stop the background service |
|
||||
| `tunnel serve` | run in the foreground |
|
||||
| `tunnel status` | local state, daemon, server state and certificate state |
|
||||
@@ -107,15 +108,25 @@ On macOS, build and ad-hoc sign a launchable bundle:
|
||||
```bash
|
||||
./scripts/build-desktop-app.sh 0.2.0
|
||||
ditto Tunnel.app "$HOME/Applications/Tunnel.app"
|
||||
open "$HOME/Applications/Tunnel.app" --args --autostart
|
||||
open "$HOME/Applications/Tunnel.app"
|
||||
```
|
||||
|
||||
Or install the notarized release:
|
||||
|
||||
```bash
|
||||
brew install --cask tzgyn-tunnel-app
|
||||
```
|
||||
|
||||
It uses `~/.config/tunnel/config.json` like the CLI, shows tunnel status and
|
||||
certificate expiry, routes with copy/open/remove buttons, live logs, settings
|
||||
and profile import, and has a start/stop button that runs the service
|
||||
in-process. The Settings dialog has **Connect on launch**; add the app to
|
||||
macOS Login Items (or the Linux/Windows equivalent) to reconnect at login. If
|
||||
the CLI service is already running, a banner offers to stop it first.
|
||||
in-process. **Check now** (and an automatic check after connecting) shows
|
||||
per-route `local`/`public` health and a summary; “local” is a TCP connect to
|
||||
the route target, “public” is a TLS handshake through the real public hostname
|
||||
(DNS + proxy + bridge + certificate). The Settings dialog has **Connect on
|
||||
launch**; add the app to macOS Login Items (or the Linux/Windows equivalent) to
|
||||
reconnect at login. If the CLI service is already running, a banner offers to
|
||||
stop it first.
|
||||
|
||||
Fyne needs CGO: the macOS build is tested; Linux and Windows must be built on
|
||||
those platforms (or with a native CGO cross toolchain), not with
|
||||
|
||||
@@ -50,6 +50,8 @@ func run(args []string) int {
|
||||
return runConfig(configPath, rest)
|
||||
case "route":
|
||||
return runRoute(configPath, rest)
|
||||
case "check":
|
||||
return runCheck(configPath, rest)
|
||||
case "import":
|
||||
return runImport(configPath, rest)
|
||||
case "up":
|
||||
@@ -87,6 +89,7 @@ Commands:
|
||||
route add <port|host:port> [--name NAME] [--proxy-protocol v1|v2]
|
||||
route remove <name>
|
||||
route list
|
||||
check [name] Check local targets and the public TLS path
|
||||
import --profile NAME --api URL
|
||||
Take over an existing official opentunnel profile
|
||||
up Provision if needed and run in the background
|
||||
@@ -335,6 +338,55 @@ func runImport(configPath string, args []string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func runCheck(configPath string, args []string) int {
|
||||
if len(args) > 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tunnel check [route]")
|
||||
return 2
|
||||
}
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
if len(cfg.Routes) == 0 {
|
||||
fmt.Println("No routes.")
|
||||
return 0
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var results []client.RouteCheck
|
||||
if len(args) == 1 {
|
||||
results = []client.RouteCheck{client.CheckRoute(ctx, cfg, args[0])}
|
||||
} else {
|
||||
results = client.CheckAll(ctx, cfg)
|
||||
}
|
||||
failed := false
|
||||
for _, result := range results {
|
||||
local, public := "down", "down"
|
||||
if result.LocalUp {
|
||||
local = "up"
|
||||
}
|
||||
if result.PublicUp {
|
||||
public = "up"
|
||||
}
|
||||
fmt.Printf("%-20s local=%-4s public=%-4s\n", result.Route, local, public)
|
||||
if result.LocalError != "" {
|
||||
fmt.Printf(" local: %s\n", result.LocalError)
|
||||
}
|
||||
if result.PublicError != "" {
|
||||
fmt.Printf(" public: %s\n", result.PublicError)
|
||||
}
|
||||
if !result.LocalUp || !result.PublicUp {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func runUp(configPath string) int {
|
||||
cfg, err := client.Load(configPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RouteCheck is the result of checking one route's local target and its
|
||||
// public TLS path.
|
||||
type RouteCheck struct {
|
||||
Route string
|
||||
LocalUp bool
|
||||
PublicUp bool
|
||||
LocalError string
|
||||
PublicError string
|
||||
Checked time.Time
|
||||
}
|
||||
|
||||
// CheckLocal dials the route target's TCP port.
|
||||
func CheckLocal(ctx context.Context, target string) (bool, string) {
|
||||
dialer := net.Dialer{Timeout: 3 * time.Second}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
conn.Close()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// CheckPublic performs a TLS handshake against the public route hostname. It
|
||||
// goes through DNS, the public proxy/relay, the bridge and the client's TLS
|
||||
// certificate, so success means the external path is up. It intentionally
|
||||
// does not send application data, so it works for non-HTTP routes too.
|
||||
func CheckPublic(ctx context.Context, publicURL string) (bool, string) {
|
||||
u, err := url.Parse(publicURL)
|
||||
if err != nil || u.Hostname() == "" {
|
||||
return false, "no public hostname"
|
||||
}
|
||||
host := u.Hostname()
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
port = "443"
|
||||
}
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
raw, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
defer raw.Close()
|
||||
// A TCP connect can succeed while the TLS handshake stalls (for example
|
||||
// when the tunnel has no bridge); bound it independently of the dialer.
|
||||
_ = raw.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
tlsConn := tls.Client(raw, &tls.Config{
|
||||
ServerName: host,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
})
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// CheckRoute checks one route.
|
||||
func CheckRoute(ctx context.Context, cfg *Config, name string) RouteCheck {
|
||||
result := RouteCheck{Route: name, Checked: time.Now()}
|
||||
route, ok := cfg.Routes[name]
|
||||
if !ok {
|
||||
result.LocalError = "route not found"
|
||||
result.PublicError = "route not found"
|
||||
return result
|
||||
}
|
||||
result.LocalUp, result.LocalError = CheckLocal(ctx, route.Target)
|
||||
result.PublicUp, result.PublicError = CheckPublic(ctx, cfg.RouteURL(name))
|
||||
return result
|
||||
}
|
||||
|
||||
// CheckAll checks every route concurrently.
|
||||
func CheckAll(ctx context.Context, cfg *Config) []RouteCheck {
|
||||
names := make([]string, 0, len(cfg.Routes))
|
||||
for name := range cfg.Routes {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
results := make([]RouteCheck, len(names))
|
||||
var wg sync.WaitGroup
|
||||
for i, name := range names {
|
||||
wg.Add(1)
|
||||
go func(i int, name string) {
|
||||
defer wg.Done()
|
||||
results[i] = CheckRoute(ctx, cfg, name)
|
||||
}(i, name)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
// CheckSummary formats the counts for a UI.
|
||||
func CheckSummary(results []RouteCheck) string {
|
||||
local, public := 0, 0
|
||||
for _, result := range results {
|
||||
if result.LocalUp {
|
||||
local++
|
||||
}
|
||||
if result.PublicUp {
|
||||
public++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("local %d/%d up · public %d/%d up", local, len(results), public, len(results))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckLocal(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
if up, detail := CheckLocal(ctx, listener.Addr().String()); !up {
|
||||
t.Errorf("listener should be up: %s", detail)
|
||||
}
|
||||
if up, _ := CheckLocal(ctx, "127.0.0.1:1"); up {
|
||||
t.Error("closed port should be down")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSummary(t *testing.T) {
|
||||
results := []RouteCheck{
|
||||
{LocalUp: true, PublicUp: true},
|
||||
{LocalUp: false, PublicUp: true},
|
||||
}
|
||||
if got := CheckSummary(results); got != "local 1/2 up · public 2/2 up" {
|
||||
t.Errorf("summary = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRouteMissing(t *testing.T) {
|
||||
cfg := &Config{Routes: map[string]Route{}, Hostname: "abc.tunnel.example"}
|
||||
result := CheckRoute(context.Background(), cfg, "missing")
|
||||
if result.LocalUp || result.PublicUp || result.LocalError == "" {
|
||||
t.Errorf("missing route result: %+v", result)
|
||||
}
|
||||
}
|
||||
+183
-4
@@ -64,8 +64,20 @@ type App struct {
|
||||
configLabel *widget.Label
|
||||
startButton *widget.Button
|
||||
routeBox *fyne.Container
|
||||
routeRows map[string]*routeRow
|
||||
checkSummary *widget.Label
|
||||
logText *widget.TextGrid
|
||||
banner fyne.CanvasObject
|
||||
|
||||
checkMu sync.Mutex
|
||||
checks map[string]client.RouteCheck
|
||||
checking bool
|
||||
lastCheck time.Time
|
||||
}
|
||||
|
||||
type routeRow struct {
|
||||
local *widget.Label
|
||||
public *widget.Label
|
||||
}
|
||||
|
||||
// Run creates and runs the desktop app.
|
||||
@@ -167,9 +179,16 @@ func (a *App) buildUI() {
|
||||
routeHeader := container.NewHBox(
|
||||
widget.NewLabelWithStyle("Routes", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
layout.NewSpacer(),
|
||||
widget.NewButtonWithIcon("Check now", theme.ViewRefreshIcon(), a.checkRoutes),
|
||||
widget.NewButtonWithIcon("Add route", theme.ContentAddIcon(), a.addRouteDialog),
|
||||
)
|
||||
a.routeBox = container.NewVBox()
|
||||
a.routeRows = map[string]*routeRow{}
|
||||
a.checks = map[string]client.RouteCheck{}
|
||||
a.checkSummary = widget.NewLabel("Not checked yet")
|
||||
a.checkSummary.Importance = widget.LowImportance
|
||||
checkDetails := widget.NewButton("Details", a.showCheckDetails)
|
||||
checkBar := container.NewHBox(a.checkSummary, checkDetails)
|
||||
a.logText = widget.NewTextGrid()
|
||||
logScroll := container.NewScroll(a.logText)
|
||||
logScroll.SetMinSize(fyne.NewSize(0, 220))
|
||||
@@ -190,6 +209,7 @@ func (a *App) buildUI() {
|
||||
a.banner,
|
||||
routeHeader,
|
||||
a.routeBox,
|
||||
checkBar,
|
||||
widget.NewLabelWithStyle("Log", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
logScroll,
|
||||
)
|
||||
@@ -230,10 +250,20 @@ func (a *App) refresh() {
|
||||
}
|
||||
a.refreshRoutes()
|
||||
a.updateStatus()
|
||||
if a.svc != nil && a.svc.CurrentStatus().Connected {
|
||||
a.maybeCheck()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) refreshRoutes() {
|
||||
a.routeBox.Objects = nil
|
||||
a.routeRows = map[string]*routeRow{}
|
||||
a.checkMu.Lock()
|
||||
checks := make(map[string]client.RouteCheck, len(a.checks))
|
||||
for name, check := range a.checks {
|
||||
checks[name] = check
|
||||
}
|
||||
a.checkMu.Unlock()
|
||||
for _, name := range a.routeNames() {
|
||||
route := a.cfg.Routes[name]
|
||||
linkURL := a.cfg.RouteURL(name)
|
||||
@@ -241,25 +271,169 @@ func (a *App) refreshRoutes() {
|
||||
if parsed, err := url.Parse(linkURL); err == nil {
|
||||
link.SetURL(parsed)
|
||||
}
|
||||
local := widget.NewLabel("local —")
|
||||
public := widget.NewLabel("public —")
|
||||
local.Importance = widget.LowImportance
|
||||
public.Importance = widget.LowImportance
|
||||
row := &routeRow{local: local, public: public}
|
||||
if check, ok := checks[name]; ok {
|
||||
setCheckLabel(local, "local", check.LocalUp, check.LocalError)
|
||||
setCheckLabel(public, "public", check.PublicUp, check.PublicError)
|
||||
}
|
||||
a.routeRows[name] = row
|
||||
copyButton := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), func() {
|
||||
a.app.Clipboard().SetContent(linkURL)
|
||||
})
|
||||
deleteButton := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
a.removeRoute(name)
|
||||
})
|
||||
row := container.NewHBox(
|
||||
container.NewGridWrap(fyne.NewSize(130, 32), widget.NewLabel(name)),
|
||||
container.NewGridWrap(fyne.NewSize(170, 32), widget.NewLabel(route.Target)),
|
||||
line := container.NewHBox(
|
||||
container.NewGridWrap(fyne.NewSize(120, 32), widget.NewLabel(name)),
|
||||
container.NewGridWrap(fyne.NewSize(150, 32), widget.NewLabel(route.Target)),
|
||||
link,
|
||||
layout.NewSpacer(),
|
||||
local,
|
||||
public,
|
||||
copyButton,
|
||||
deleteButton,
|
||||
)
|
||||
a.routeBox.Add(row)
|
||||
a.routeBox.Add(line)
|
||||
}
|
||||
a.routeBox.Refresh()
|
||||
}
|
||||
|
||||
func setCheckLabel(label *widget.Label, kind string, up bool, detail string) {
|
||||
if up {
|
||||
label.SetText(kind + " ✓")
|
||||
label.Importance = widget.SuccessImportance
|
||||
} else {
|
||||
label.SetText(kind + " ✗")
|
||||
label.Importance = widget.DangerImportance
|
||||
}
|
||||
label.Refresh()
|
||||
_ = detail
|
||||
}
|
||||
|
||||
func (a *App) checkRoutes() {
|
||||
if a.cfg == nil {
|
||||
return
|
||||
}
|
||||
routes := make(map[string]client.Route, len(a.cfg.Routes))
|
||||
for name, route := range a.cfg.Routes {
|
||||
routes[name] = route
|
||||
}
|
||||
if len(routes) == 0 {
|
||||
a.checkSummary.SetText("No routes")
|
||||
a.checkSummary.Importance = widget.LowImportance
|
||||
a.checkSummary.Refresh()
|
||||
return
|
||||
}
|
||||
snapshot := &client.Config{API: a.cfg.API, Hostname: a.cfg.Hostname, Routes: routes}
|
||||
a.checkMu.Lock()
|
||||
if a.checking {
|
||||
a.checkMu.Unlock()
|
||||
return
|
||||
}
|
||||
a.checking = true
|
||||
a.checkMu.Unlock()
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
results := client.CheckAll(ctx, snapshot)
|
||||
checks := make(map[string]client.RouteCheck, len(results))
|
||||
for _, result := range results {
|
||||
checks[result.Route] = result
|
||||
}
|
||||
a.checkMu.Lock()
|
||||
a.checks = checks
|
||||
a.lastCheck = time.Now()
|
||||
a.checking = false
|
||||
a.checkMu.Unlock()
|
||||
fyne.Do(a.applyChecks)
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) maybeCheck() {
|
||||
a.checkMu.Lock()
|
||||
due := !a.checking && time.Since(a.lastCheck) > 30*time.Second
|
||||
a.checkMu.Unlock()
|
||||
if due {
|
||||
a.checkRoutes()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) applyChecks() {
|
||||
a.checkMu.Lock()
|
||||
checks := make(map[string]client.RouteCheck, len(a.checks))
|
||||
for name, check := range a.checks {
|
||||
checks[name] = check
|
||||
}
|
||||
a.checkMu.Unlock()
|
||||
|
||||
results := make([]client.RouteCheck, 0, len(a.routeRows))
|
||||
for name, row := range a.routeRows {
|
||||
check, ok := checks[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
setCheckLabel(row.local, "local", check.LocalUp, check.LocalError)
|
||||
setCheckLabel(row.public, "public", check.PublicUp, check.PublicError)
|
||||
results = append(results, check)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
a.checkSummary.SetText("Not checked yet")
|
||||
a.checkSummary.Importance = widget.LowImportance
|
||||
} else {
|
||||
a.checkSummary.SetText(client.CheckSummary(results))
|
||||
a.checkSummary.Importance = widget.SuccessImportance
|
||||
for _, result := range results {
|
||||
if !result.LocalUp || !result.PublicUp {
|
||||
a.checkSummary.Importance = widget.DangerImportance
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
a.checkSummary.Refresh()
|
||||
}
|
||||
|
||||
func (a *App) showCheckDetails() {
|
||||
a.checkMu.Lock()
|
||||
checks := make(map[string]client.RouteCheck, len(a.checks))
|
||||
for name, check := range a.checks {
|
||||
checks[name] = check
|
||||
}
|
||||
a.checkMu.Unlock()
|
||||
lines := []string{}
|
||||
for _, name := range a.routeNames() {
|
||||
check, ok := checks[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if check.LocalError != "" {
|
||||
lines = append(lines, fmt.Sprintf("%s local: %s", name, check.LocalError))
|
||||
}
|
||||
if check.PublicError != "" {
|
||||
lines = append(lines, fmt.Sprintf("%s public: %s", name, check.PublicError))
|
||||
}
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
lines = []string{"No errors recorded."}
|
||||
}
|
||||
a.showInfo(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func (a *App) resetChecks() {
|
||||
a.checkMu.Lock()
|
||||
a.checks = map[string]client.RouteCheck{}
|
||||
a.checking = false
|
||||
a.lastCheck = time.Time{}
|
||||
a.checkMu.Unlock()
|
||||
a.checkSummary.SetText("Not checked yet")
|
||||
a.checkSummary.Importance = widget.LowImportance
|
||||
a.checkSummary.Refresh()
|
||||
}
|
||||
|
||||
func (a *App) refreshLog() {
|
||||
a.logText.SetText(a.logWriter.text(300))
|
||||
}
|
||||
@@ -307,6 +481,9 @@ func (a *App) updateStatus() {
|
||||
a.dot.Refresh()
|
||||
a.startButton.Enable()
|
||||
a.refreshRoutes()
|
||||
if status.Connected {
|
||||
a.maybeCheck()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) isRunning() bool {
|
||||
@@ -471,6 +648,7 @@ func (a *App) addRouteDialog() {
|
||||
a.showError(err)
|
||||
return
|
||||
}
|
||||
a.resetChecks()
|
||||
a.refresh()
|
||||
if !a.isRunning() && a.cfg.API != "" {
|
||||
a.start()
|
||||
@@ -487,6 +665,7 @@ func (a *App) removeRoute(name string) {
|
||||
a.showError(err)
|
||||
return
|
||||
}
|
||||
a.resetChecks()
|
||||
a.refresh()
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ if [ ! -x "$FYNE" ]; then
|
||||
FYNE="$(go env GOPATH)/bin/fyne"
|
||||
fi
|
||||
|
||||
mkdir -p .local
|
||||
go build -o .local/Tunnel -ldflags "-X main.version=$VERSION" ./cmd/tunnel-desktop
|
||||
mkdir -p .local/desktop
|
||||
go build -o .local/desktop/Tunnel -ldflags "-X main.version=$VERSION" ./cmd/tunnel-desktop
|
||||
rm -rf Tunnel.app
|
||||
"$FYNE" package -os darwin \
|
||||
-executable .local/Tunnel \
|
||||
-executable .local/desktop/Tunnel \
|
||||
-name Tunnel -app-id dev.tzgyn.tunnel \
|
||||
-app-version "$VERSION" -app-build "$BUILD" \
|
||||
-icon assets/icon.png -release
|
||||
|
||||
@@ -21,11 +21,11 @@ FYNE="$(command -v fyne || true)"
|
||||
IDENT="$(security find-identity -v -p codesigning | awk '/Developer ID Application:/{print $2; exit}')"
|
||||
[ -n "$IDENT" ] || { echo "no Developer ID Application identity"; exit 1; }
|
||||
|
||||
mkdir -p "$OUT"
|
||||
go build -o .local/Tunnel -ldflags "-X main.version=$VERSION" ./cmd/tunnel-desktop
|
||||
mkdir -p "$OUT" .local/desktop
|
||||
go build -o .local/desktop/Tunnel -ldflags "-X main.version=$VERSION" ./cmd/tunnel-desktop
|
||||
rm -rf "$APP"
|
||||
"$FYNE" package -os darwin \
|
||||
-executable .local/Tunnel \
|
||||
-executable .local/desktop/Tunnel \
|
||||
-name Tunnel -app-id dev.tzgyn.tunnel \
|
||||
-app-version "$VERSION" -app-build "$BUILD" \
|
||||
-icon assets/icon.png -release
|
||||
|
||||
Reference in New Issue
Block a user