From 3e8cf39903775ecb6b8ebe03d43591453fe3c7d8 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Fri, 26 Apr 2019 20:05:24 +0200 Subject: ui: simplify everything Signed-off-by: Jason A. Donenfeld --- ui/confview.go | 55 ++----- ui/iconprovider.go | 326 +++++++++++++++++++++++++++++++++++++++ ui/logpage.go | 80 +++++----- ui/manage_tunnels.go | 96 ------------ ui/managewindow.go | 103 +++++++++++++ ui/tray.go | 173 +++++++++++++-------- ui/tunnelspage.go | 102 +++--------- ui/tunnelstatusimageprovider.go | 332 ---------------------------------------- ui/tunnelsview.go | 92 +++++++++-- ui/tunneltracker.go | 99 ------------ ui/ui.go | 56 ++----- 11 files changed, 702 insertions(+), 812 deletions(-) create mode 100644 ui/iconprovider.go delete mode 100644 ui/manage_tunnels.go create mode 100644 ui/managewindow.go delete mode 100644 ui/tunnelstatusimageprovider.go delete mode 100644 ui/tunneltracker.go (limited to 'ui') diff --git a/ui/confview.go b/ui/confview.go index aa939945..5dd5c626 100644 --- a/ui/confview.go +++ b/ui/confview.go @@ -31,7 +31,6 @@ type labelStatusLine struct { statusComposite *walk.Composite statusImage *walk.ImageView statusLabel *walk.TextLabel - imageProvider *TunnelStatusImageProvider } type labelTextLine struct { @@ -40,9 +39,8 @@ type labelTextLine struct { } type toggleActiveLine struct { - composite *walk.Composite - button *walk.PushButton - tunnelTracker *TunnelTracker + composite *walk.Composite + button *walk.PushButton } type interfaceView struct { @@ -77,20 +75,15 @@ type ConfView struct { tunnel *service.Tunnel } -func (lsl *labelStatusLine) Dispose() { - if lsl.imageProvider != nil { - lsl.imageProvider.Dispose() - lsl.imageProvider = nil - } -} - func (lsl *labelStatusLine) widgets() (walk.Widget, walk.Widget) { return lsl.label, lsl.statusComposite } func (lsl *labelStatusLine) update(state service.TunnelState) { - img, _ := lsl.imageProvider.ImageForState(state, walk.Size{statusImageSize, statusImageSize}) - lsl.statusImage.SetImage(img) + img, err := iconProvider.ImageForState(state, walk.Size{statusImageSize, statusImageSize}) + if err == nil { + lsl.statusImage.SetImage(img) + } switch state { case service.TunnelStarted: @@ -109,7 +102,6 @@ func (lsl *labelStatusLine) update(state service.TunnelState) { func newLabelStatusLine(parent walk.Container) *labelStatusLine { lsl := new(labelStatusLine) - parent.AddDisposable(lsl) lsl.label, _ = walk.NewTextLabel(parent) lsl.label.SetText("Status:") @@ -120,7 +112,6 @@ func newLabelStatusLine(parent walk.Container) *labelStatusLine { layout.SetMargins(walk.Margins{}) lsl.statusComposite.SetLayout(layout) - lsl.imageProvider, _ = NewTunnelStatusImageProvider() lsl.statusImage, _ = walk.NewImageView(lsl.statusComposite) lsl.statusLabel, _ = walk.NewTextLabel(lsl.statusComposite) lsl.statusLabel.SetTextAlignment(walk.AlignHNearVCenter) @@ -192,10 +183,6 @@ func (tal *toggleActiveLine) update(state service.TunnelState) { enabled, text = false, "" } - if tt := tal.tunnelTracker; tt != nil && tt.InTransition() { - enabled = false - } - tal.button.SetEnabled(enabled) tal.button.SetText(text) tal.button.SetVisible(state != service.TunnelUnknown) @@ -416,32 +403,18 @@ func (cv *ConfView) Dispose() { cv.ScrollView.Dispose() } -func (cv *ConfView) TunnelTracker() *TunnelTracker { - return cv.interfaze.toggleActive.tunnelTracker -} - -func (cv *ConfView) SetTunnelTracker(tunnelTracker *TunnelTracker) { - cv.interfaze.toggleActive.tunnelTracker = tunnelTracker -} - func (cv *ConfView) onToggleActiveClicked() { cv.interfaze.toggleActive.button.SetEnabled(false) - - var title string - var err error - tt := cv.TunnelTracker() - if activeTunnel := tt.ActiveTunnel(); activeTunnel != nil && activeTunnel.Name == cv.tunnel.Name { - title = "Failed to deactivate tunnel" - err = tt.DeactivateTunnel() - } else { - title = "Failed to activate tunnel" - err = tt.ActivateTunnel(cv.tunnel) - } + oldState, err := cv.tunnel.Toggle() if err != nil { - walk.MsgBox(cv.Form(), title, err.Error(), walk.MsgBoxIconError) - return + if oldState == service.TunnelUnknown { + walk.MsgBox(cv.Form(), "Failed to determine tunnel state", err.Error(), walk.MsgBoxIconError) + } else if oldState == service.TunnelStopped { + walk.MsgBox(cv.Form(), "Failed to activate tunnel", err.Error(), walk.MsgBoxIconError) + } else if oldState == service.TunnelStarted { + walk.MsgBox(cv.Form(), "Failed to deactivate tunnel", err.Error(), walk.MsgBoxIconError) + } } - cv.SetTunnel(cv.tunnel) } diff --git a/ui/iconprovider.go b/ui/iconprovider.go new file mode 100644 index 00000000..da88e830 --- /dev/null +++ b/ui/iconprovider.go @@ -0,0 +1,326 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package ui + +import ( + "github.com/lxn/walk" + "golang.zx2c4.com/wireguard/windows/service" + "math" +) + +type sizeAndState struct { + size walk.Size + state service.TunnelState +} + +type IconProvider struct { + baseIcon *walk.Icon + imagesBySizeAndState map[sizeAndState]*walk.Bitmap + iconsByState map[service.TunnelState]*walk.Icon + stoppedBrush *walk.SolidColorBrush + startingBrush *walk.SolidColorBrush + startedBrush *walk.SolidColorBrush + stoppedPen *walk.CosmeticPen + startingPen *walk.CosmeticPen + startedPen *walk.CosmeticPen +} + +const ( + colorStopped = 0xe1e1e1 + colorStarting = 0xfec031 + colorStarted = 0x36ce42 +) + +func hexColor(c uint32) walk.Color { + return walk.Color((((c >> 16) & 0xff) << 0) | (((c >> 8) & 0xff) << 8) | (((c >> 0) & 0xff) << 16)) +} + +func darkColor(c walk.Color) walk.Color { + // Convert to HSL + r, g, b := float64((uint32(c)>>16)&0xff)/255.0, float64((uint32(c)>>8)&0xff)/255.0, float64((uint32(c)>>0)&0xff)/255.0 + min := math.Min(r, math.Min(g, b)) + max := math.Max(r, math.Max(g, b)) + deltaMinMax := max - min + l := (max + min) / 2 + h, s := 0.0, 0.0 + if deltaMinMax != 0 { + if l < 0.5 { + s = deltaMinMax / (max + min) + } else { + s = deltaMinMax / (2 - max - min) + } + deltaRed := (((max - r) / 6) + (deltaMinMax / 2)) / deltaMinMax + deltaGreen := (((max - g) / 6) + (deltaMinMax / 2)) / deltaMinMax + deltaBlue := (((max - b) / 6) + (deltaMinMax / 2)) / deltaMinMax + if r == max { + h = deltaBlue - deltaGreen + } else if g == max { + h = (1.0 / 3.0) + deltaRed - deltaBlue + } else if b == max { + h = (2.0 / 3.0) + deltaGreen - deltaRed + } + + if h < 0 { + h += 1 + } else if h > 1 { + h -= 1 + } + } + + // Darken by 10% + l = math.Max(0, l-0.1) + + // Convert back to RGB + if s == 0 { + return walk.Color((uint32(l*255) << 16) | (uint32(l*255) << 8) | (uint32(l*255) << 0)) + } + var v1, v2 float64 + if l < 0.5 { + v2 = l * (1 + s) + } else { + v2 = (l + s) - (s * l) + } + v1 = 2.0*l - v2 + co := func(v1, v2, vH float64) float64 { + if vH < 0 { + vH += 1 + } + if vH > 1 { + vH -= 1 + } + if (6.0 * vH) < 1 { + return v1 + (v2-v1)*6.0*vH + } + if (2.0 * vH) < 1 { + return v2 + } + if (3.0 * vH) < 2 { + return v1 + (v2-v1)*((2.0/3.0)-vH)*6.0 + } + return v1 + } + r, g, b = co(v1, v2, h+(1.0/3.0)), co(v1, v2, h), co(v1, v2, h-(1.0/3.0)) + return walk.Color((uint32(r*255) << 16) | (uint32(g*255) << 8) | (uint32(b*255) << 0)) +} + +func NewIconProvider() (*IconProvider, error) { + tsip := &IconProvider{ + imagesBySizeAndState: make(map[sizeAndState]*walk.Bitmap), + iconsByState: make(map[service.TunnelState]*walk.Icon), + } + + var err error + + var disposables walk.Disposables + defer disposables.Treat() + + if tsip.baseIcon, err = walk.NewIconFromResourceId(1); err != nil { + return nil, err + } + disposables.Add(tsip.baseIcon) + + if tsip.stoppedBrush, err = walk.NewSolidColorBrush(hexColor(colorStopped)); err != nil { + return nil, err + } + disposables.Add(tsip.stoppedBrush) + + if tsip.startingBrush, err = walk.NewSolidColorBrush(hexColor(colorStarting)); err != nil { + return nil, err + } + disposables.Add(tsip.startingBrush) + + if tsip.startedBrush, err = walk.NewSolidColorBrush(hexColor(colorStarted)); err != nil { + return nil, err + } + disposables.Add(tsip.startedBrush) + + if tsip.stoppedPen, err = walk.NewCosmeticPen(walk.PenSolid, darkColor(hexColor(colorStopped))); err != nil { + return nil, err + } + disposables.Add(tsip.stoppedPen) + + if tsip.startingPen, err = walk.NewCosmeticPen(walk.PenSolid, darkColor(hexColor(colorStarting))); err != nil { + return nil, err + } + disposables.Add(tsip.startingPen) + + if tsip.startedPen, err = walk.NewCosmeticPen(walk.PenSolid, darkColor(hexColor(colorStarted))); err != nil { + return nil, err + } + disposables.Add(tsip.startedPen) + + disposables.Spare() + + return tsip, nil +} + +func (tsip *IconProvider) Dispose() { + if tsip.imagesBySizeAndState != nil { + for _, img := range tsip.imagesBySizeAndState { + img.Dispose() + } + tsip.imagesBySizeAndState = nil + } + if tsip.iconsByState != nil { + for _, icon := range tsip.iconsByState { + icon.Dispose() + } + tsip.iconsByState = nil + } + if tsip.stoppedBrush != nil { + tsip.stoppedBrush.Dispose() + tsip.stoppedBrush = nil + } + if tsip.startingBrush != nil { + tsip.startingBrush.Dispose() + tsip.startingBrush = nil + } + if tsip.startedBrush != nil { + tsip.startedBrush.Dispose() + tsip.startedBrush = nil + } + if tsip.stoppedPen != nil { + tsip.stoppedPen.Dispose() + tsip.stoppedPen = nil + } + if tsip.startingPen != nil { + tsip.startingPen.Dispose() + tsip.startingPen = nil + } + if tsip.startedPen != nil { + tsip.startedPen.Dispose() + tsip.startedPen = nil + } + if tsip.baseIcon != nil { + tsip.baseIcon.Dispose() + tsip.baseIcon = nil + } +} + +func (tsip *IconProvider) ImageForTunnel(tunnel *service.Tunnel, size walk.Size) (*walk.Bitmap, error) { + state, err := tunnel.State() + if err != nil { + return nil, err + } + + return tsip.ImageForState(state, size) +} + +func (tsip *IconProvider) ImageForState(state service.TunnelState, size walk.Size) (*walk.Bitmap, error) { + key := sizeAndState{size, state} + + if img, ok := tsip.imagesBySizeAndState[key]; ok { + return img, nil + } + + img, err := walk.NewBitmapWithTransparentPixels(size) + if err != nil { + return nil, err + } + + canvas, err := walk.NewCanvasFromImage(img) + if err != nil { + return nil, err + } + defer canvas.Dispose() + + if err := tsip.PaintForState(state, canvas, walk.Rectangle{0, 0, size.Width, size.Height}); err != nil { + return nil, err + } + + tsip.imagesBySizeAndState[key] = img + + return img, nil +} + +func (tsip *IconProvider) IconWithOverlayForState(state service.TunnelState) (*walk.Icon, error) { + if icon, ok := tsip.iconsByState[state]; ok { + return icon, nil + } + + size := tsip.baseIcon.Size() + + bmp, err := walk.NewBitmapWithTransparentPixels(size) + if err != nil { + return nil, err + } + defer bmp.Dispose() + + canvas, err := walk.NewCanvasFromImage(bmp) + if err != nil { + return nil, err + } + defer canvas.Dispose() + + if err := canvas.DrawImage(tsip.baseIcon, walk.Point{}); err != nil { + return nil, err + } + + w := int(float64(size.Width) * 0.75) + h := int(float64(size.Height) * 0.75) + bounds := walk.Rectangle{4 + size.Width - w, 4 + size.Height - h, w, h} + + if err := tsip.PaintForState(state, canvas, bounds); err != nil { + return nil, err + } + + canvas.Dispose() + + icon, err := walk.NewIconFromBitmap(bmp) + if err != nil { + return nil, err + } + + tsip.iconsByState[state] = icon + + return icon, nil +} + +func (tsip *IconProvider) PaintForTunnel(tunnel *service.Tunnel, canvas *walk.Canvas, bounds walk.Rectangle) error { + state, err := tunnel.State() + if err != nil { + return err + } + + return tsip.PaintForState(state, canvas, bounds) +} + +func (tsip *IconProvider) PaintForState(state service.TunnelState, canvas *walk.Canvas, bounds walk.Rectangle) error { + var ( + brush *walk.SolidColorBrush + pen *walk.CosmeticPen + ) + + switch state { + case service.TunnelStarted: + brush = tsip.startedBrush + pen = tsip.startedPen + + case service.TunnelStopped: + brush = tsip.stoppedBrush + pen = tsip.stoppedPen + + default: + brush = tsip.startingBrush + pen = tsip.startingPen + } + + b := bounds + + b.X += 4 + b.Y += 4 + b.Height -= 8 + b.Width = b.Height + + if err := canvas.FillEllipse(brush, b); err != nil { + return err + } + if err := canvas.DrawEllipse(pen, b); err != nil { + return err + } + + return nil +} diff --git a/ui/logpage.go b/ui/logpage.go index 4f39ae87..d3b57a70 100644 --- a/ui/logpage.go +++ b/ui/logpage.go @@ -17,8 +17,12 @@ import ( "golang.zx2c4.com/wireguard/windows/ringlogger" ) -func NewLogPage(logger *ringlogger.Ringlogger) (*LogPage, error) { - lp := &LogPage{logger: logger} +const ( + maxLogLinesDisplayed = 10000 +) + +func NewLogPage() (*LogPage, error) { + lp := &LogPage{} var disposables walk.Disposables defer disposables.Treat() @@ -56,7 +60,7 @@ func NewLogPage(logger *ringlogger.Ringlogger) (*LogPage, error) { msgCol.SetTitle("Log message") lp.logView.Columns().Add(msgCol) - lp.model = newLogModel(lp, logger) + lp.model = newLogModel(lp) lp.logView.SetModel(lp.model) buttonsContainer, err := walk.NewComposite(lp) @@ -83,12 +87,11 @@ func NewLogPage(logger *ringlogger.Ringlogger) (*LogPage, error) { type LogPage struct { *walk.TabPage logView *walk.TableView - logger *ringlogger.Ringlogger model *logModel } func (lp *LogPage) isAtBottom() bool { - return lp.logView.ItemVisible(len(lp.model.items) - 1) + return len(lp.model.items) == 0 || lp.logView.ItemVisible(len(lp.model.items)-1) } func (lp *LogPage) scrollToBottom() { @@ -108,13 +111,12 @@ func (lp *LogPage) onSaveButtonClicked() { return } - extensions := []string{".log", ".txt"} - if fd.FilterIndex < 3 && !strings.HasSuffix(fd.FilePath, extensions[fd.FilterIndex-1]) { - fd.FilePath = fd.FilePath + extensions[fd.FilterIndex-1] + if fd.FilterIndex == 1 && !strings.HasSuffix(fd.FilePath, ".txt") { + fd.FilePath = fd.FilePath + ".txt" } writeFileWithOverwriteHandling(form, fd.FilePath, func(file *os.File) error { - if _, err := lp.logger.WriteTo(file); err != nil { + if _, err := ringlogger.Global.WriteTo(file); err != nil { return fmt.Errorf("exportLog: Ringlogger.WriteTo failed: %v", err) } @@ -124,50 +126,38 @@ func (lp *LogPage) onSaveButtonClicked() { type logModel struct { walk.ReflectTableModelBase - lp *LogPage - quit chan bool - logger *ringlogger.Ringlogger - items []ringlogger.FollowLine + lp *LogPage + quit chan bool + items []ringlogger.FollowLine } -func newLogModel(lp *LogPage, logger *ringlogger.Ringlogger) *logModel { - mdl := &logModel{lp: lp, quit: make(chan bool), logger: logger} - var lastCursor uint32 - mdl.items, lastCursor = logger.FollowFromCursor(ringlogger.CursorAll) - - var lastStamp time.Time - if len(mdl.items) > 0 { - lastStamp = mdl.items[len(mdl.items)-1].Stamp - } - +func newLogModel(lp *LogPage) *logModel { + mdl := &logModel{lp: lp, quit: make(chan bool)} go func() { - ticker := time.NewTicker(time.Second) + ticker := time.NewTicker(time.Millisecond * 300) + cursor := ringlogger.CursorAll + var items []ringlogger.FollowLine for { select { case <-ticker.C: - items, cursor := mdl.logger.FollowFromCursor(ringlogger.CursorAll) - - var stamp time.Time - if len(items) > 0 { - stamp = items[len(items)-1].Stamp - } - - if cursor != lastCursor || stamp.After(lastStamp) { - lastCursor = cursor - lastStamp = stamp - - mdl.lp.Synchronize(func() { - isAtBottom := mdl.lp.isAtBottom() - - mdl.items = items - mdl.PublishRowsReset() - - if isAtBottom { - mdl.lp.scrollToBottom() - } - }) + items, cursor = ringlogger.Global.FollowFromCursor(cursor) + if len(items) == 0 { + continue } + mdl.lp.Synchronize(func() { + isAtBottom := mdl.lp.isAtBottom() + + mdl.items = append(mdl.items, items...) + if len(mdl.items) > maxLogLinesDisplayed { + mdl.items = mdl.items[len(mdl.items)-maxLogLinesDisplayed:] + } + mdl.PublishRowsReset() + + if isAtBottom { + mdl.lp.scrollToBottom() + } + }) case <-mdl.quit: ticker.Stop() diff --git a/ui/manage_tunnels.go b/ui/manage_tunnels.go deleted file mode 100644 index 584cbd9b..00000000 --- a/ui/manage_tunnels.go +++ /dev/null @@ -1,96 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. - */ - -package ui - -import ( - "github.com/lxn/walk" - "github.com/lxn/win" - "golang.zx2c4.com/wireguard/windows/ringlogger" - "golang.zx2c4.com/wireguard/windows/service" -) - -type ManageTunnelsWindow struct { - *walk.MainWindow - - icon *walk.Icon - logger *ringlogger.Ringlogger - tunnelsPage *TunnelsPage - logPage *LogPage -} - -func NewManageTunnelsWindow(icon *walk.Icon, logger *ringlogger.Ringlogger) (*ManageTunnelsWindow, error) { - var err error - - var disposables walk.Disposables - defer disposables.Treat() - - mtw := &ManageTunnelsWindow{ - icon: icon, - logger: logger, - } - mtw.MainWindow, err = walk.NewMainWindowWithName("WireGuard") - if err != nil { - return nil, err - } - disposables.Add(mtw) - - mtw.SetIcon(mtw.icon) - mtw.SetTitle("WireGuard") - font, err := walk.NewFont("Segoe UI", 9, 0) - if err != nil { - return nil, err - } - mtw.AddDisposable(font) - mtw.SetFont(font) - mtw.SetSize(walk.Size{900, 600}) - mtw.SetLayout(walk.NewVBoxLayout()) - mtw.Closing().Attach(func(canceled *bool, reason walk.CloseReason) { - // "Close to tray" instead of exiting application - onQuit() - }) - mtw.VisibleChanged().Attach(func() { - if mtw.Visible() { - mtw.tunnelsPage.updateConfView() - win.SetForegroundWindow(mtw.Handle()) - win.BringWindowToTop(mtw.Handle()) - - mtw.logPage.scrollToBottom() - } - }) - - tabWidget, _ := walk.NewTabWidget(mtw) - - mtw.tunnelsPage, _ = NewTunnelsPage() - tabWidget.Pages().Add(mtw.tunnelsPage.TabPage) - - mtw.logPage, _ = NewLogPage(logger) - tabWidget.Pages().Add(mtw.logPage.TabPage) - - disposables.Spare() - - return mtw, nil -} - -func (mtw *ManageTunnelsWindow) TunnelTracker() *TunnelTracker { - return mtw.tunnelsPage.tunnelTracker -} - -func (mtw *ManageTunnelsWindow) SetTunnelTracker(tunnelTracker *TunnelTracker) { - mtw.tunnelsPage.tunnelTracker = tunnelTracker - - mtw.tunnelsPage.confView.SetTunnelTracker(tunnelTracker) -} - -func (mtw *ManageTunnelsWindow) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState) { - mtw.tunnelsPage.SetTunnelState(tunnel, state) - - icon, err := mtw.tunnelsPage.tunnelsView.imageProvider.IconWithOverlayForState(mtw.icon, state) - if err != nil { - return - } - - mtw.SetIcon(icon) -} diff --git a/ui/managewindow.go b/ui/managewindow.go new file mode 100644 index 00000000..85c55844 --- /dev/null +++ b/ui/managewindow.go @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package ui + +import ( + "github.com/lxn/walk" + "github.com/lxn/win" + "golang.zx2c4.com/wireguard/windows/service" +) + +type ManageTunnelsWindow struct { + *walk.MainWindow + + tunnelsPage *TunnelsPage + logPage *LogPage + + tunnelChangedCB *service.TunnelChangeCallback +} + +func NewManageTunnelsWindow() (*ManageTunnelsWindow, error) { + var err error + + var disposables walk.Disposables + defer disposables.Treat() + + mtw := &ManageTunnelsWindow{} + + mtw.MainWindow, err = walk.NewMainWindowWithName("WireGuard") + if err != nil { + return nil, err + } + disposables.Add(mtw) + + mtw.SetIcon(iconProvider.baseIcon) + mtw.SetTitle("WireGuard") + font, err := walk.NewFont("Segoe UI", 9, 0) + if err != nil { + return nil, err + } + mtw.AddDisposable(font) + mtw.SetFont(font) + mtw.SetSize(walk.Size{900, 600}) + mtw.SetLayout(walk.NewVBoxLayout()) + mtw.Closing().Attach(func(canceled *bool, reason walk.CloseReason) { + // "Close to tray" instead of exiting application + onQuit() + }) + mtw.VisibleChanged().Attach(func() { + if mtw.Visible() { + mtw.tunnelsPage.updateConfView() + win.SetForegroundWindow(mtw.Handle()) + win.BringWindowToTop(mtw.Handle()) + + mtw.logPage.scrollToBottom() + } + }) + + tabWidget, _ := walk.NewTabWidget(mtw) + + mtw.tunnelsPage, _ = NewTunnelsPage() + tabWidget.Pages().Add(mtw.tunnelsPage.TabPage) + + mtw.logPage, _ = NewLogPage() + tabWidget.Pages().Add(mtw.logPage.TabPage) + + disposables.Spare() + + mtw.tunnelChangedCB = service.IPCClientRegisterTunnelChange(mtw.onTunnelChange) + mtw.onTunnelChange(nil, service.TunnelUnknown, nil) + + return mtw, nil +} + +func (mtw *ManageTunnelsWindow) Dispose() { + if mtw.tunnelChangedCB != nil { + mtw.tunnelChangedCB.Unregister() + mtw.tunnelChangedCB = nil + } + mtw.MainWindow.Dispose() +} + +func (mtw *ManageTunnelsWindow) onTunnelChange(tunnel *service.Tunnel, state service.TunnelState, err error) { + globalState, err2 := service.IPCClientGlobalState() + mtw.Synchronize(func() { + if err2 == nil { + icon, err2 := iconProvider.IconWithOverlayForState(globalState) + if err2 == nil { + mtw.SetIcon(icon) + } + } + + if err != nil && mtw.Visible() { + errMsg := err.Error() + if len(errMsg) > 0 && errMsg[len(errMsg)-1] != '.' { + errMsg += "." + } + walk.MsgBox(mtw, "Tunnel Error", errMsg+"\n\nPlease consult the log for more information.", walk.MsgBoxIconWarning) + } + }) +} diff --git a/ui/tray.go b/ui/tray.go index 2650530d..3204d861 100644 --- a/ui/tray.go +++ b/ui/tray.go @@ -14,8 +14,8 @@ import ( "golang.zx2c4.com/wireguard/windows/service" ) -// Status + active CIDRs + deactivate + separator -const trayTunnelActionsOffset = 4 +// Status + active CIDRs + separator +const trayTunnelActionsOffset = 3 type Tray struct { *walk.NotifyIcon @@ -23,18 +23,20 @@ type Tray struct { // Current known tunnels by name tunnels map[string]*walk.Action - mtw *ManageTunnelsWindow - icon *walk.Icon + mtw *ManageTunnelsWindow + + tunnelChangedCB *service.TunnelChangeCallback + tunnelsChangedCB *service.TunnelsChangeCallback } -func NewTray(mtw *ManageTunnelsWindow, icon *walk.Icon) (*Tray, error) { +func NewTray(mtw *ManageTunnelsWindow) (*Tray, error) { var err error tray := &Tray{ mtw: mtw, - icon: icon, tunnels: make(map[string]*walk.Action), } + tray.NotifyIcon, err = walk.NewNotifyIcon(mtw.MainWindow) if err != nil { return nil, err @@ -46,7 +48,7 @@ func NewTray(mtw *ManageTunnelsWindow, icon *walk.Icon) (*Tray, error) { func (tray *Tray) setup() error { tray.SetToolTip("WireGuard: Deactivated") tray.SetVisible(true) - tray.SetIcon(tray.icon) + tray.SetIcon(iconProvider.baseIcon) tray.MouseDown().Attach(func(x, y int, button walk.MouseButton) { if button == walk.LeftButton { @@ -64,7 +66,6 @@ func (tray *Tray) setup() error { }{ {label: "Status: Unknown"}, {label: "Networks: None", hidden: true}, - {label: "Deactivate", handler: tray.onDeactivateTunnel, enabled: true, hidden: true}, {separator: true}, {separator: true}, {label: "&Manage tunnels...", handler: tray.mtw.Show, enabled: true}, @@ -88,54 +89,94 @@ func (tray *Tray) setup() error { tray.ContextMenu().Actions().Add(action) } + tray.tunnelChangedCB = service.IPCClientRegisterTunnelChange(tray.onTunnelChange) + tray.tunnelsChangedCB = service.IPCClientRegisterTunnelsChange(tray.onTunnelsChange) + tray.onTunnelsChange() + tray.updateGlobalState() - tunnels, err := service.IPCClientTunnels() - if err != nil { - return err - } + return nil +} - for _, tunnel := range tunnels { - tray.addTunnelAction(tunnel.Name) +func (tray *Tray) Dispose() error { + if tray.tunnelChangedCB != nil { + tray.tunnelChangedCB.Unregister() + tray.tunnelChangedCB = nil } + if tray.tunnelsChangedCB != nil { + tray.tunnelsChangedCB.Unregister() + tray.tunnelsChangedCB = nil + } + return tray.NotifyIcon.Dispose() +} - tray.mtw.tunnelsPage.TunnelAdded().Attach(tray.addTunnelAction) - tray.mtw.tunnelsPage.TunnelDeleted().Attach(tray.removeTunnelAction) - - return nil +func (tray *Tray) onTunnelsChange() { + tunnels, err := service.IPCClientTunnels() + if err != nil { + return + } + tray.mtw.Synchronize(func() { + tunnelSet := make(map[string]bool, len(tunnels)) + for _, tunnel := range tunnels { + tunnelSet[tunnel.Name] = true + if tray.tunnels[tunnel.Name] == nil { + tray.addTunnelAction(&tunnel) + } + } + for trayTunnel := range tray.tunnels { + if !tunnelSet[trayTunnel] { + tray.removeTunnelAction(trayTunnel) + } + } + }) } -func (tray *Tray) addTunnelAction(tunnelName string) { +func (tray *Tray) addTunnelAction(tunnel *service.Tunnel) { tunnelAction := walk.NewAction() - tunnelAction.SetText(tunnelName) + tunnelAction.SetText(tunnel.Name) tunnelAction.SetEnabled(true) tunnelAction.SetCheckable(true) + tclosure := *tunnel tunnelAction.Triggered().Attach(func() { - if activeTunnel := tray.mtw.tunnelsPage.tunnelTracker.activeTunnel; activeTunnel != nil && activeTunnel.Name == tunnelName { - tray.onDeactivateTunnel() - } else { - tray.onActivateTunnel(tunnelName) + oldState, err := tclosure.Toggle() + if err != nil { + tray.mtw.Show() + //TODO: select tunnel that we're showing the error for in mtw + if oldState == service.TunnelUnknown { + walk.MsgBox(tray.mtw, "Failed to determine tunnel state", err.Error(), walk.MsgBoxIconError) + } else if oldState == service.TunnelStopped { + walk.MsgBox(tray.mtw, "Failed to activate tunnel", err.Error(), walk.MsgBoxIconError) + } else if oldState == service.TunnelStarted { + walk.MsgBox(tray.mtw, "Failed to deactivate tunnel", err.Error(), walk.MsgBoxIconError) + } + return } }) - tray.tunnels[tunnelName] = tunnelAction + tray.tunnels[tunnel.Name] = tunnelAction // Add the action at the right spot var names []string - for name, _ := range tray.tunnels { + for name := range tray.tunnels { names = append(names, name) } - sort.Strings(names) + sort.Strings(names) //TODO: use correct sorting order for this var ( idx int name string ) for idx, name = range names { - if name == tunnelName { + if name == tunnel.Name { break } } tray.ContextMenu().Actions().Insert(trayTunnelActionsOffset+idx, tunnelAction) + + state, err := tunnel.State() + if err != nil { + return + } + tray.SetTunnelState(tunnel, state, false) } func (tray *Tray) removeTunnelAction(tunnelName string) { @@ -143,21 +184,28 @@ func (tray *Tray) removeTunnelAction(tunnelName string) { delete(tray.tunnels, tunnelName) } -func (tray *Tray) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState) { - tray.SetTunnelStateWithNotification(tunnel, state, true) +func (tray *Tray) onTunnelChange(tunnel *service.Tunnel, state service.TunnelState, err error) { + tray.mtw.Synchronize(func() { + tray.SetTunnelState(tunnel, state, err == nil) + if !tray.mtw.Visible() && err != nil { + tray.ShowError("WireGuard Tunnel Error", err.Error()) + } + }) } -func (tray *Tray) SetTunnelStateWithNotification(tunnel *service.Tunnel, state service.TunnelState, showNotifications bool) { - if icon, err := tray.mtw.tunnelsPage.tunnelsView.imageProvider.IconWithOverlayForState(tray.icon, state); err == nil { - tray.SetIcon(icon) +func (tray *Tray) updateGlobalState() { + state, err := service.IPCClientGlobalState() + if err != nil { + return } - tunnelAction := tray.tunnels[tunnel.Name] + if icon, err := iconProvider.IconWithOverlayForState(state); err == nil { + tray.SetIcon(icon) + } actions := tray.ContextMenu().Actions() statusAction := actions.At(0) activeCIDRsAction := actions.At(1) - deactivateAction := actions.At(2) setTunnelActionsEnabled := func(enabled bool) { for i := 0; i < len(tray.tunnels); i++ { @@ -170,16 +218,37 @@ func (tray *Tray) SetTunnelStateWithNotification(tunnel *service.Tunnel, state s case service.TunnelStarting: statusAction.SetText("Status: Activating") setTunnelActionsEnabled(false) - tray.SetToolTip("WireGuard: Activating...") case service.TunnelStarted: + activeCIDRsAction.SetVisible(err == nil) statusAction.SetText("Status: Active") setTunnelActionsEnabled(true) + tray.SetToolTip("WireGuard: Activated") + + case service.TunnelStopping: + statusAction.SetText("Status: Deactivating") + setTunnelActionsEnabled(false) + tray.SetToolTip("WireGuard: Deactivating...") + + case service.TunnelStopped: + activeCIDRsAction.SetVisible(false) + statusAction.SetText("Status: Inactive") + setTunnelActionsEnabled(true) + tray.SetToolTip("WireGuard: Deactivated") + } +} +func (tray *Tray) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState, showNotifications bool) { + tunnelAction := tray.tunnels[tunnel.Name] + + actions := tray.ContextMenu().Actions() + activeCIDRsAction := actions.At(1) + + switch state { + case service.TunnelStarted: + activeCIDRsAction.SetText("") config, err := tunnel.RuntimeConfig() - activeCIDRsAction.SetVisible(err == nil) - deactivateAction.SetVisible(err == nil) if err == nil { var sb strings.Builder for i, addr := range config.Interface.Addresses { @@ -189,46 +258,20 @@ func (tray *Tray) SetTunnelStateWithNotification(tunnel *service.Tunnel, state s sb.WriteString(addr.String()) } - activeCIDRsAction.SetText(fmt.Sprintf("Networks: %s", sb.String())) } - tunnelAction.SetEnabled(true) tunnelAction.SetChecked(true) - - tray.SetToolTip("WireGuard: Activated") if showNotifications { tray.ShowInfo("WireGuard Activated", fmt.Sprintf("The %s tunnel has been activated.", tunnel.Name)) } - case service.TunnelStopping: - statusAction.SetText("Status: Deactivating") - setTunnelActionsEnabled(false) - - tray.SetToolTip("WireGuard: Deactivating...") - case service.TunnelStopped: - statusAction.SetText("Status: Inactive") - activeCIDRsAction.SetVisible(false) - deactivateAction.SetVisible(false) - setTunnelActionsEnabled(true) tunnelAction.SetChecked(false) - - tray.SetToolTip("WireGuard: Deactivated") if showNotifications { tray.ShowInfo("WireGuard Deactivated", fmt.Sprintf("The %s tunnel has been deactivated.", tunnel.Name)) } } -} -func (tray *Tray) onActivateTunnel(tunnelName string) { - if err := tray.mtw.TunnelTracker().ActivateTunnel(&service.Tunnel{tunnelName}); err != nil { - walk.MsgBox(tray.mtw, "Failed to activate tunnel", err.Error(), walk.MsgBoxIconError) - } -} - -func (tray *Tray) onDeactivateTunnel() { - if err := tray.mtw.TunnelTracker().DeactivateTunnel(); err != nil { - walk.MsgBox(tray.mtw, "Failed to deactivate tunnel", err.Error(), walk.MsgBoxIconError) - } + tray.updateGlobalState() } diff --git a/ui/tunnelspage.go b/ui/tunnelspage.go index ac778b3a..a3ecf0c0 100644 --- a/ui/tunnelspage.go +++ b/ui/tunnelspage.go @@ -22,11 +22,8 @@ import ( type TunnelsPage struct { *walk.TabPage - tunnelTracker *TunnelTracker - tunnelsView *TunnelsView - confView *ConfView - tunnelAddedPublisher walk.StringEventPublisher - tunnelDeletedPublisher walk.StringEventPublisher + tunnelsView *TunnelsView + confView *ConfView } func NewTunnelsPage() (*TunnelsPage, error) { @@ -128,20 +125,6 @@ func NewTunnelsPage() (*TunnelsPage, error) { return tp, nil } -func (tp *TunnelsPage) TunnelTracker() *TunnelTracker { - return tp.tunnelTracker -} - -func (tp *TunnelsPage) SetTunnelTracker(tunnelTracker *TunnelTracker) { - tp.tunnelTracker = tunnelTracker - - tp.confView.SetTunnelTracker(tunnelTracker) -} - -func (tp *TunnelsPage) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState) { - tp.tunnelsView.SetTunnelState(tunnel, state) -} - func (tp *TunnelsPage) updateConfView() { if !tp.Visible() { return @@ -261,71 +244,33 @@ func (tp *TunnelsPage) exportTunnels(filePath string) { } func (tp *TunnelsPage) addTunnel(config *conf.Config) { - tunnel, err := service.IPCClientNewTunnel(config) + _, err := service.IPCClientNewTunnel(config) if err != nil { walk.MsgBox(tp.Form(), "Unable to create tunnel", err.Error(), walk.MsgBoxIconError) - return } - model := tp.tunnelsView.model - model.tunnels = append(model.tunnels, tunnel) - model.PublishRowsReset() - model.Sort(model.SortedColumn(), model.SortOrder()) - - for i, t := range model.tunnels { - if t.Name == tunnel.Name { - tp.tunnelsView.SetCurrentIndex(i) - break - } - } - - tp.confView.SetTunnel(&tunnel) - - tp.tunnelAddedPublisher.Publish(tunnel.Name) } func (tp *TunnelsPage) deleteTunnel(tunnel *service.Tunnel) { - tunnel.Delete() - - model := tp.tunnelsView.model - - for i, t := range model.tunnels { - if t.Name == tunnel.Name { - model.tunnels = append(model.tunnels[:i], model.tunnels[i+1:]...) - model.PublishRowsRemoved(i, i) - break - } + err := tunnel.Delete() + if err != nil { + walk.MsgBox(tp.Form(), "Unable to delete tunnel", err.Error(), walk.MsgBoxIconError) } - - tp.tunnelDeletedPublisher.Publish(tunnel.Name) -} - -func (tp *TunnelsPage) TunnelAdded() *walk.StringEvent { - return tp.tunnelAddedPublisher.Event() -} - -func (tp *TunnelsPage) TunnelDeleted() *walk.StringEvent { - return tp.tunnelDeletedPublisher.Event() } // Handlers func (tp *TunnelsPage) onTunnelsViewItemActivated() { - if tp.tunnelTracker.InTransition() { - return - } - - var err error - var title string - tunnel := tp.tunnelsView.CurrentTunnel() - activeTunnel := tp.tunnelTracker.ActiveTunnel() - if tunnel != nil && activeTunnel != nil && tunnel.Name == activeTunnel.Name { - err, title = tp.tunnelTracker.DeactivateTunnel(), "Deactivating tunnel failed" - } else { - err, title = tp.tunnelTracker.ActivateTunnel(tunnel), "Activating tunnel failed" - } + oldState, err := tp.tunnelsView.CurrentTunnel().Toggle() if err != nil { - walk.MsgBox(tp.Form(), title, fmt.Sprintf("Error: %s", err.Error()), walk.MsgBoxIconError) + if oldState == service.TunnelUnknown { + walk.MsgBox(tp.Form(), "Failed to determine tunnel state", err.Error(), walk.MsgBoxIconError) + } else if oldState == service.TunnelStopped { + walk.MsgBox(tp.Form(), "Failed to activate tunnel", err.Error(), walk.MsgBoxIconError) + } else if oldState == service.TunnelStarted { + walk.MsgBox(tp.Form(), "Failed to deactivate tunnel", err.Error(), walk.MsgBoxIconError) + } + return } } @@ -337,11 +282,16 @@ func (tp *TunnelsPage) onEditTunnel() { } if config := runTunnelConfigDialog(tp.Form(), tunnel); config != nil { - // Delete old one - tp.deleteTunnel(tunnel) - - // Save new one - tp.addTunnel(config) + go func() { + priorState, err := tunnel.State() + tunnel.Delete() + tunnel.WaitForStop() + tunnel, err2 := service.IPCClientNewTunnel(config) + if err == nil && err2 == nil && (priorState == service.TunnelStarting || priorState == service.TunnelStarted) { + tunnel.Start() + } + //TODO: synchronize and select newly added tunnel + }() } } @@ -368,8 +318,6 @@ func (tp *TunnelsPage) onDelete() { } tp.deleteTunnel(currentTunnel) - - tp.tunnelDeletedPublisher.Publish(currentTunnel.Name) } func (tp *TunnelsPage) onImport() { diff --git a/ui/tunnelstatusimageprovider.go b/ui/tunnelstatusimageprovider.go deleted file mode 100644 index 1120ecea..00000000 --- a/ui/tunnelstatusimageprovider.go +++ /dev/null @@ -1,332 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. - */ - -package ui - -import ( - "github.com/lxn/walk" - "golang.zx2c4.com/wireguard/windows/service" - "math" -) - -type sizeAndState struct { - size walk.Size - state service.TunnelState -} - -type iconAndState struct { - icon *walk.Icon - state service.TunnelState -} - -type TunnelStatusImageProvider struct { - imagesBySizeAndState map[sizeAndState]*walk.Bitmap - iconsByBaseIconAndState map[iconAndState]*walk.Icon - stoppedBrush *walk.SolidColorBrush - startingBrush *walk.SolidColorBrush - startedBrush *walk.SolidColorBrush - stoppedPen *walk.CosmeticPen - startingPen *walk.CosmeticPen - startedPen *walk.CosmeticPen -} - -const ( - colorStopped = 0xe1e1e1 - colorStarting = 0xfec031 - colorStarted = 0x36ce42 -) - -func hexColor(c uint32) walk.Color { - return walk.Color((((c >> 16) & 0xff) << 0) | (((c >> 8) & 0xff) << 8) | (((c >> 0) & 0xff) << 16)) -} - -func darkColor(c walk.Color) walk.Color { - // Convert to HSL - r, g, b := float64((uint32(c)>>16)&0xff)/255.0, float64((uint32(c)>>8)&0xff)/255.0, float64((uint32(c)>>0)&0xff)/255.0 - min := math.Min(r, math.Min(g, b)) - max := math.Max(r, math.Max(g, b)) - deltaMinMax := max - min - l := (max + min) / 2 - h, s := 0.0, 0.0 - if deltaMinMax != 0 { - if l < 0.5 { - s = deltaMinMax / (max + min) - } else { - s = deltaMinMax / (2 - max - min) - } - deltaRed := (((max - r) / 6) + (deltaMinMax / 2)) / deltaMinMax - deltaGreen := (((max - g) / 6) + (deltaMinMax / 2)) / deltaMinMax - deltaBlue := (((max - b) / 6) + (deltaMinMax / 2)) / deltaMinMax - if r == max { - h = deltaBlue - deltaGreen - } else if g == max { - h = (1.0 / 3.0) + deltaRed - deltaBlue - } else if b == max { - h = (2.0 / 3.0) + deltaGreen - deltaRed - } - - if h < 0 { - h += 1 - } else if h > 1 { - h -= 1 - } - } - - // Darken by 10% - l = math.Max(0, l-0.1) - - // Convert back to RGB - if s == 0 { - return walk.Color((uint32(l*255) << 16) | (uint32(l*255) << 8) | (uint32(l*255) << 0)) - } - var v1, v2 float64 - if l < 0.5 { - v2 = l * (1 + s) - } else { - v2 = (l + s) - (s * l) - } - v1 = 2.0*l - v2 - co := func(v1, v2, vH float64) float64 { - if vH < 0 { - vH += 1 - } - if vH > 1 { - vH -= 1 - } - if (6.0 * vH) < 1 { - return v1 + (v2-v1)*6.0*vH - } - if (2.0 * vH) < 1 { - return v2 - } - if (3.0 * vH) < 2 { - return v1 + (v2-v1)*((2.0/3.0)-vH)*6.0 - } - return v1 - } - r, g, b = co(v1, v2, h+(1.0/3.0)), co(v1, v2, h), co(v1, v2, h-(1.0/3.0)) - return walk.Color((uint32(r*255) << 16) | (uint32(g*255) << 8) | (uint32(b*255) << 0)) -} - -func NewTunnelStatusImageProvider() (*TunnelStatusImageProvider, error) { - tsip := &TunnelStatusImageProvider{ - imagesBySizeAndState: make(map[sizeAndState]*walk.Bitmap), - iconsByBaseIconAndState: make(map[iconAndState]*walk.Icon), - } - - var err error - - var disposables walk.Disposables - defer disposables.Treat() - - if tsip.stoppedBrush, err = walk.NewSolidColorBrush(hexColor(colorStopped)); err != nil { - return nil, err - } - disposables.Add(tsip.stoppedBrush) - - if tsip.startingBrush, err = walk.NewSolidColorBrush(hexColor(colorStarting)); err != nil { - return nil, err - } - disposables.Add(tsip.startingBrush) - - if tsip.startedBrush, err = walk.NewSolidColorBrush(hexColor(colorStarted)); err != nil { - return nil, err - } - disposables.Add(tsip.startedBrush) - - if tsip.stoppedPen, err = walk.NewCosmeticPen(walk.PenSolid, darkColor(hexColor(colorStopped))); err != nil { - return nil, err - } - disposables.Add(tsip.stoppedPen) - - if tsip.startingPen, err = walk.NewCosmeticPen(walk.PenSolid, darkColor(hexColor(colorStarting))); err != nil { - return nil, err - } - disposables.Add(tsip.startingPen) - - if tsip.startedPen, err = walk.NewCosmeticPen(walk.PenSolid, darkColor(hexColor(colorStarted))); err != nil { - return nil, err - } - disposables.Add(tsip.startedPen) - - disposables.Spare() - - return tsip, nil -} - -func (tsip *TunnelStatusImageProvider) Dispose() { - if tsip.imagesBySizeAndState != nil { - for _, img := range tsip.imagesBySizeAndState { - img.Dispose() - } - tsip.imagesBySizeAndState = nil - } - if tsip.iconsByBaseIconAndState != nil { - for _, icon := range tsip.iconsByBaseIconAndState { - icon.Dispose() - } - tsip.iconsByBaseIconAndState = nil - } - if tsip.stoppedBrush != nil { - tsip.stoppedBrush.Dispose() - tsip.stoppedBrush = nil - } - if tsip.startingBrush != nil { - tsip.startingBrush.Dispose() - tsip.startingBrush = nil - } - if tsip.startedBrush != nil { - tsip.startedBrush.Dispose() - tsip.startedBrush = nil - } - if tsip.stoppedPen != nil { - tsip.stoppedPen.Dispose() - tsip.stoppedPen = nil - } - if tsip.startingPen != nil { - tsip.startingPen.Dispose() - tsip.startingPen = nil - } - if tsip.startedPen != nil { - tsip.startedPen.Dispose() - tsip.startedPen = nil - } -} - -func (tsip *TunnelStatusImageProvider) ImageForTunnel(tunnel *service.Tunnel, size walk.Size) (*walk.Bitmap, error) { - state, err := tunnel.State() - if err != nil { - return nil, err - } - - return tsip.ImageForState(state, size) -} - -func (tsip *TunnelStatusImageProvider) ImageForState(state service.TunnelState, size walk.Size) (*walk.Bitmap, error) { - key := sizeAndState{size, state} - - if img, ok := tsip.imagesBySizeAndState[key]; ok { - return img, nil - } - - img, err := walk.NewBitmapWithTransparentPixels(size) - if err != nil { - return nil, err - } - - canvas, err := walk.NewCanvasFromImage(img) - if err != nil { - return nil, err - } - defer canvas.Dispose() - - if err := tsip.PaintForState(state, canvas, walk.Rectangle{0, 0, size.Width, size.Height}); err != nil { - return nil, err - } - - tsip.imagesBySizeAndState[key] = img - - return img, nil -} - -func (tsip *TunnelStatusImageProvider) IconWithOverlayForTunnel(baseIcon *walk.Icon, tunnel *service.Tunnel) (*walk.Icon, error) { - state, err := tunnel.State() - if err != nil { - return nil, err - } - - return tsip.IconWithOverlayForState(baseIcon, state) -} - -func (tsip *TunnelStatusImageProvider) IconWithOverlayForState(baseIcon *walk.Icon, state service.TunnelState) (*walk.Icon, error) { - key := iconAndState{baseIcon, state} - - if icon, ok := tsip.iconsByBaseIconAndState[key]; ok { - return icon, nil - } - - size := baseIcon.Size() - - bmp, err := walk.NewBitmapWithTransparentPixels(size) - if err != nil { - return nil, err - } - defer bmp.Dispose() - - canvas, err := walk.NewCanvasFromImage(bmp) - if err != nil { - return nil, err - } - defer canvas.Dispose() - - if err := canvas.DrawImage(baseIcon, walk.Point{}); err != nil { - return nil, err - } - - w := int(float64(size.Width) * 0.75) - h := int(float64(size.Height) * 0.75) - bounds := walk.Rectangle{4 + size.Width - w, 4 + size.Height - h, w, h} - - if err := tsip.PaintForState(state, canvas, bounds); err != nil { - return nil, err - } - - canvas.Dispose() - - icon, err := walk.NewIconFromBitmap(bmp) - if err != nil { - return nil, err - } - - tsip.iconsByBaseIconAndState[key] = icon - - return icon, nil -} - -func (tsip *TunnelStatusImageProvider) PaintForTunnel(tunnel *service.Tunnel, canvas *walk.Canvas, bounds walk.Rectangle) error { - state, err := tunnel.State() - if err != nil { - return err - } - - return tsip.PaintForState(state, canvas, bounds) -} - -func (tsip *TunnelStatusImageProvider) PaintForState(state service.TunnelState, canvas *walk.Canvas, bounds walk.Rectangle) error { - var ( - brush *walk.SolidColorBrush - pen *walk.CosmeticPen - ) - - switch state { - case service.TunnelStarted: - brush = tsip.startedBrush - pen = tsip.startedPen - - case service.TunnelStarting: - brush = tsip.startingBrush - pen = tsip.startingPen - - default: - brush = tsip.stoppedBrush - pen = tsip.stoppedPen - } - - b := bounds - - b.X += 4 - b.Y += 4 - b.Height -= 8 - b.Width = b.Height - - if err := canvas.FillEllipse(brush, b); err != nil { - return err - } - if err := canvas.DrawEllipse(pen, b); err != nil { - return err - } - - return nil -} diff --git a/ui/tunnelsview.go b/ui/tunnelsview.go index 2f6ea2a4..c5fe5bb5 100644 --- a/ui/tunnelsview.go +++ b/ui/tunnelsview.go @@ -38,6 +38,7 @@ func (t *TunnelModel) Value(row, col int) interface{} { func (t *TunnelModel) Sort(col int, order walk.SortOrder) error { sort.SliceStable(t.tunnels, func(i, j int) bool { + //TODO: use real string comparison for sorting with proper tunnel order return t.tunnels[i].Name < t.tunnels[j].Name }) @@ -47,8 +48,10 @@ func (t *TunnelModel) Sort(col int, order walk.SortOrder) error { type TunnelsView struct { *walk.TableView - model *TunnelModel - imageProvider *TunnelStatusImageProvider + model *TunnelModel + + tunnelChangedCB *service.TunnelChangeCallback + tunnelsChangedCB *service.TunnelsChangeCallback } func NewTunnelsView(parent walk.Container) (*TunnelsView, error) { @@ -77,18 +80,29 @@ func NewTunnelsView(parent walk.Container) (*TunnelsView, error) { model: model, } - if tunnelsView.imageProvider, err = NewTunnelStatusImageProvider(); err != nil { - return nil, err - } - tunnelsView.AddDisposable(tunnelsView.imageProvider) - tv.SetCellStyler(tunnelsView) disposables.Spare() + tunnelsView.tunnelChangedCB = service.IPCClientRegisterTunnelChange(tunnelsView.onTunnelChange) + tunnelsView.tunnelsChangedCB = service.IPCClientRegisterTunnelsChange(tunnelsView.onTunnelsChange) + tunnelsView.onTunnelsChange() + return tunnelsView, nil } +func (tv *TunnelsView) Dispose() { + if tv.tunnelChangedCB != nil { + tv.tunnelChangedCB.Unregister() + tv.tunnelChangedCB = nil + } + if tv.tunnelsChangedCB != nil { + tv.tunnelsChangedCB.Unregister() + tv.tunnelsChangedCB = nil + } + tv.TableView.Dispose() +} + func (tv *TunnelsView) StyleCell(style *walk.CellStyle) { canvas := style.Canvas() if canvas == nil { @@ -106,7 +120,7 @@ func (tv *TunnelsView) StyleCell(style *walk.CellStyle) { b.X = 0 b.Width = b.Height - tv.imageProvider.PaintForTunnel(tunnel, canvas, b) + iconProvider.PaintForTunnel(tunnel, canvas, b) } func (tv *TunnelsView) CurrentTunnel() *service.Tunnel { @@ -118,17 +132,61 @@ func (tv *TunnelsView) CurrentTunnel() *service.Tunnel { return &tv.model.tunnels[idx] } -func (tv *TunnelsView) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState) { - idx := -1 - for i, _ := range tv.model.tunnels { - if tv.model.tunnels[i].Name == tunnel.Name { - idx = i - break +func (tv *TunnelsView) onTunnelChange(tunnel *service.Tunnel, state service.TunnelState, err error) { + tv.Synchronize(func() { + idx := -1 + for i := range tv.model.tunnels { + if tv.model.tunnels[i].Name == tunnel.Name { + idx = i + break + } } - } - if idx != -1 { - tv.model.PublishRowChanged(idx) + if idx != -1 { + tv.model.PublishRowChanged(idx) + return + } + }) +} + +func (tv *TunnelsView) onTunnelsChange() { + tunnels, err := service.IPCClientTunnels() + if err != nil { return } + tv.Synchronize(func() { + newTunnels := make(map[service.Tunnel]bool, len(tunnels)) + oldTunnels := make(map[service.Tunnel]bool, len(tv.model.tunnels)) + for _, tunnel := range tunnels { + newTunnels[tunnel] = true + } + for _, tunnel := range tv.model.tunnels { + oldTunnels[tunnel] = true + } + + for tunnel := range oldTunnels { + if !newTunnels[tunnel] { + for i, t := range tv.model.tunnels { + //TODO: this is inefficient. Use a map here instead. + if t.Name == tunnel.Name { + tv.model.tunnels = append(tv.model.tunnels[:i], tv.model.tunnels[i+1:]...) + tv.model.PublishRowsRemoved(i, i) + break + } + } + } + } + didAdd := false + for tunnel := range newTunnels { + if !oldTunnels[tunnel] { + tv.model.tunnels = append(tv.model.tunnels, tunnel) + didAdd = true + //TODO: If adding a tunnel for the first time when the previously were none, select it + } + } + if didAdd { + tv.model.PublishRowsReset() + tv.model.Sort(tv.model.SortedColumn(), tv.model.SortOrder()) + } + }) } diff --git a/ui/tunneltracker.go b/ui/tunneltracker.go deleted file mode 100644 index 73d48538..00000000 --- a/ui/tunneltracker.go +++ /dev/null @@ -1,99 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. - */ - -package ui - -import ( - "fmt" - - "github.com/lxn/walk" - "golang.zx2c4.com/wireguard/windows/service" -) - -type TunnelTracker struct { - activeTunnel *service.Tunnel - activeTunnelChanged walk.EventPublisher - tunnelChangeCB *service.TunnelChangeCallback - inTransition bool -} - -func (tt *TunnelTracker) ActiveTunnel() *service.Tunnel { - return tt.activeTunnel -} - -func (tt *TunnelTracker) ActivateTunnel(tunnel *service.Tunnel) error { - if tunnel == tt.activeTunnel { - return nil - } - - if err := tt.DeactivateTunnel(); err != nil { - return fmt.Errorf("ActivateTunnel: Failed to deactivate tunnel '%s': %v", tunnel.Name, err) - } - - if err := tunnel.Start(); err != nil { - return fmt.Errorf("ActivateTunnel: Failed to start tunnel '%s': %v", tunnel.Name, err) - } - - return nil -} - -func (tt *TunnelTracker) DeactivateTunnel() error { - if tt.activeTunnel == nil { - return nil - } - - state, err := tt.activeTunnel.State() - if err != nil { - return fmt.Errorf("DeactivateTunnel: Failed to retrieve state for tunnel %s: %v", tt.activeTunnel.Name, err) - } - - if state == service.TunnelStarted { - if err := tt.activeTunnel.Stop(); err != nil { - return fmt.Errorf("DeactivateTunnel: Failed to stop tunnel '%s': %v", tt.activeTunnel.Name, err) - } - } - - if state == service.TunnelStarted || state == service.TunnelStopping { - if err := tt.activeTunnel.WaitForStop(); err != nil { - return fmt.Errorf("DeactivateTunnel: Failed to wait for tunnel '%s' to stop: %v", tt.activeTunnel.Name, err) - } - } - - return nil -} - -func (tt *TunnelTracker) ActiveTunnelChanged() *walk.Event { - return tt.activeTunnelChanged.Event() -} - -func (tt *TunnelTracker) InTransition() bool { - return tt.inTransition -} - -func (tt *TunnelTracker) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState, err error) { - if err != nil { - tt.inTransition = false - } - - switch state { - case service.TunnelStarted: - tt.inTransition = false - tt.activeTunnel = tunnel - - case service.TunnelStarting, service.TunnelStopping: - tt.inTransition = true - - case service.TunnelStopped: - if tt.activeTunnel != nil && tt.activeTunnel.Name == tunnel.Name { - tt.inTransition = false - } - tt.activeTunnel = nil - - default: - return - } - - tt.activeTunnelChanged.Publish() -} diff --git a/ui/ui.go b/ui/ui.go index 58b52e46..d186a905 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -7,72 +7,50 @@ package ui import ( "fmt" - "os" "runtime" + "runtime/debug" "github.com/lxn/walk" "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/windows/ringlogger" "golang.zx2c4.com/wireguard/windows/service" ) // #include "../version.h" import "C" +var iconProvider *IconProvider + func RunUI() { runtime.LockOSThread() - logger, err := ringlogger.NewRingloggerFromInheritedMappingHandle(os.Args[5], "GUI") - if err != nil { - walk.MsgBox(nil, "Unable to initialize logging", fmt.Sprint(err), walk.MsgBoxIconError) - return - } + defer func() { + if err := recover(); err != nil { + walk.MsgBox(nil, "Panic", fmt.Sprint(err, "\n\n", string(debug.Stack())), walk.MsgBoxIconError) + panic(err) + } + }() - tunnelTracker := new(TunnelTracker) + var err error - icon, err := walk.NewIconFromResourceId(1) + iconProvider, err = NewIconProvider() if err != nil { - panic(err) + walk.MsgBox(nil, "Unable to initialize icon provider", fmt.Sprint(err), walk.MsgBoxIconError) + return } - defer icon.Dispose() + defer iconProvider.Dispose() - mtw, err := NewManageTunnelsWindow(icon, logger) + mtw, err := NewManageTunnelsWindow() if err != nil { panic(err) } defer mtw.Dispose() - mtw.SetTunnelTracker(tunnelTracker) - - tray, err := NewTray(mtw, icon) + tray, err := NewTray(mtw) if err != nil { panic(err) } defer tray.Dispose() - // Bind to updates - service.IPCClientRegisterTunnelChange(func(tunnel *service.Tunnel, state service.TunnelState, err error) { - mtw.Synchronize(func() { - tunnelTracker.SetTunnelState(tunnel, state, err) - mtw.SetTunnelState(tunnel, state) - tray.SetTunnelStateWithNotification(tunnel, state, err == nil) - }) - - if err == nil { - return - } - - if mtw.Visible() { - errMsg := err.Error() - if len(errMsg) > 0 && errMsg[len(errMsg)-1] != '.' { - errMsg += "." - } - walk.MsgBox(mtw, "Tunnel Error", errMsg+"\n\nPlease consult the log for more information.", walk.MsgBoxIconWarning) - } else { - tray.ShowError("WireGuard Tunnel Error", err.Error()) - } - }) - mtw.Run() } @@ -80,9 +58,7 @@ func onQuit() { _, err := service.IPCClientQuit(true) if err != nil { walk.MsgBox(nil, "Error Exiting WireGuard", fmt.Sprintf("Unable to exit service due to: %s. You may want to stop WireGuard from the service manager.", err), walk.MsgBoxIconError) - os.Exit(1) } - walk.App().Exit(0) } -- cgit v1.2.3-59-g8ed1b