From 9961f74ec8f30a5f16e6fae102b1898fdfa72f80 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 14 Aug 2022 11:08:35 -0500 Subject: [PATCH] `skywire-cli vpn` subcommands + separate-systray (#1317) * add flags for skywire-cli vpn list * remove accidentally included files * establish transport before starting VPN * update vpn list cli command logic * add skywire-cli vpn stop after successful tests with skywire-cli vpn start ; fix make check errors * improve detection of vpn-client app config in api with range loop instead of hardcoding index ; make format check * remove extra logging from cmd/skywire-visor/commands/root.go * make separate-systray work with vpn cli commands * update cmd/skywire-cli/commands/vpn/vvpn.go * add version flag for skywire-cli ; filter results of skywire-cli vpn list by buildinfo.Version() instead of hardcoded version by default * update cmd/skywire-systray/skywire-systray.go to match restructured skywire-cli subcommands * make format check * skywire-cli visor vpn list query filtering * comment out query filtering ; make format check --- .../commands/{hv => dmsgpty}/hvdmsg.go | 11 +- cmd/skywire-cli/commands/dmsgpty/root.go | 22 +- cmd/skywire-cli/commands/hv/hvpk.go | 43 -- cmd/skywire-cli/commands/hv/hvskychat.go | 24 - cmd/skywire-cli/commands/hv/hvui.go | 21 - cmd/skywire-cli/commands/hv/hvvpn.go | 97 ---- cmd/skywire-cli/commands/hv/root.go | 38 -- cmd/skywire-cli/commands/mdisc/root.go | 3 + cmd/skywire-cli/commands/root.go | 12 +- cmd/skywire-cli/commands/rpc/root.go | 25 + cmd/skywire-cli/commands/rtfind/root.go | 3 + cmd/skywire-cli/commands/visor/app.go | 11 +- cmd/skywire-cli/commands/visor/exec.go | 3 +- cmd/skywire-cli/commands/visor/info.go | 11 +- cmd/skywire-cli/commands/visor/root.go | 21 - cmd/skywire-cli/commands/visor/route.go | 9 +- cmd/skywire-cli/commands/visor/shutdown.go | 4 +- cmd/skywire-cli/commands/visor/transports.go | 44 +- cmd/skywire-cli/commands/vpn/root.go | 24 + cmd/skywire-cli/commands/vpn/vvpn.go | 209 +++++++++ cmd/skywire-systray/icons/icon.ico | Bin 0 -> 67646 bytes cmd/skywire-systray/icons/icon.png | Bin 0 -> 7025 bytes cmd/skywire-systray/icons/icon.tiff | Bin 0 -> 35948 bytes cmd/skywire-systray/skywire-systray.go | 430 ++++++++++++++++++ pkg/servicedisc/autoconnect.go | 1 + pkg/servicedisc/client.go | 17 + pkg/visor/api.go | 63 ++- pkg/visor/rpc.go | 31 +- pkg/visor/rpc_client.go | 37 +- 29 files changed, 903 insertions(+), 311 deletions(-) rename cmd/skywire-cli/commands/{hv => dmsgpty}/hvdmsg.go (90%) delete mode 100644 cmd/skywire-cli/commands/hv/hvpk.go delete mode 100644 cmd/skywire-cli/commands/hv/hvskychat.go delete mode 100644 cmd/skywire-cli/commands/hv/hvui.go delete mode 100644 cmd/skywire-cli/commands/hv/hvvpn.go delete mode 100644 cmd/skywire-cli/commands/hv/root.go create mode 100644 cmd/skywire-cli/commands/rpc/root.go create mode 100644 cmd/skywire-cli/commands/vpn/root.go create mode 100644 cmd/skywire-cli/commands/vpn/vvpn.go create mode 100644 cmd/skywire-systray/icons/icon.ico create mode 100644 cmd/skywire-systray/icons/icon.png create mode 100644 cmd/skywire-systray/icons/icon.tiff create mode 100644 cmd/skywire-systray/skywire-systray.go diff --git a/cmd/skywire-cli/commands/hv/hvdmsg.go b/cmd/skywire-cli/commands/dmsgpty/hvdmsg.go similarity index 90% rename from cmd/skywire-cli/commands/hv/hvdmsg.go rename to cmd/skywire-cli/commands/dmsgpty/hvdmsg.go index 1715682067..70429b79c9 100644 --- a/cmd/skywire-cli/commands/hv/hvdmsg.go +++ b/cmd/skywire-cli/commands/dmsgpty/hvdmsg.go @@ -1,4 +1,4 @@ -package clihv +package clidmsgpty import ( "fmt" @@ -11,9 +11,7 @@ import ( ) func init() { - RootCmd.AddCommand(dmsgCmd) - dmsgCmd.PersistentFlags().StringVarP(&rpcAddr, "rpc", "", "localhost:3435", "RPC server address") - dmsgCmd.AddCommand( + RootCmd.AddCommand( dmsgUICmd, dmsgURLCmd, ) @@ -25,11 +23,6 @@ func init() { dmsgURLCmd.Flags().StringVarP(&pk, "visor", "v", "", "public key of visor to connect to") } -var dmsgCmd = &cobra.Command{ - Use: "dmsg", - Short: "DMSGPTY UI", -} - var dmsgUICmd = &cobra.Command{ Use: "ui", Short: "Open dmsgpty UI in default browser", diff --git a/cmd/skywire-cli/commands/dmsgpty/root.go b/cmd/skywire-cli/commands/dmsgpty/root.go index 462db352f7..14e50da221 100644 --- a/cmd/skywire-cli/commands/dmsgpty/root.go +++ b/cmd/skywire-cli/commands/dmsgpty/root.go @@ -17,14 +17,22 @@ import ( "github.com/skycoin/skywire/pkg/visor" ) -var rpcAddr string -var ptyPort string -var masterLogger = logging.NewMasterLogger() -var packageLogger = masterLogger.PackageLogger("dmsgpty") +var ( + ptyPort string + masterLogger = logging.NewMasterLogger() + packageLogger = masterLogger.PackageLogger("dmsgpty") + logger = logging.MustGetLogger("skywire-cli") + rpcAddr string + path string + pk string + url string + pkg bool +) func init() { - RootCmd.PersistentFlags().StringVarP(&rpcAddr, "rpc", "", "localhost:3435", "RPC server address") - RootCmd.PersistentFlags().StringVarP(&ptyPort, "port", "p", "22", "port of remote visor dmsgpty") + visorsCmd.PersistentFlags().StringVarP(&rpcAddr, "rpc", "", "localhost:3435", "RPC server address") + shellCmd.PersistentFlags().StringVarP(&rpcAddr, "rpc", "", "localhost:3435", "RPC server address") + shellCmd.PersistentFlags().StringVarP(&ptyPort, "port", "p", "22", "port of remote visor dmsgpty") } // RootCmd is the command that contains sub-commands which interacts with dmsgpty. @@ -61,7 +69,7 @@ var visorsCmd = &cobra.Command{ var shellCmd = &cobra.Command{ Use: "start ", - Short: "Start dmsgpty for specific visor by its dmsg address pk:port", + Short: "Start dmsgpty session", Args: cobra.MinimumNArgs(1), RunE: func(_ *cobra.Command, args []string) error { cli := dmsgpty.DefaultCLI() diff --git a/cmd/skywire-cli/commands/hv/hvpk.go b/cmd/skywire-cli/commands/hv/hvpk.go deleted file mode 100644 index a2bc1477ab..0000000000 --- a/cmd/skywire-cli/commands/hv/hvpk.go +++ /dev/null @@ -1,43 +0,0 @@ -package clihv - -import ( - "fmt" - "net" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/skycoin/skywire-utilities/pkg/netutil" - "github.com/skycoin/skywire/pkg/visor" -) - -func init() { - if os.Getenv("SKYBIAN") == "true" { - RootCmd.AddCommand(pkCmd) - } - localIPs, err = netutil.DefaultNetworkInterfaceIPs() - if err != nil { - logger.WithError(err).Fatalln("Could not determine network interface IP address") - } - if len(localIPs) == 0 { - localIPs = append(localIPs, net.ParseIP("192.168.0.1")) - } - var s string - if idx := strings.LastIndex(localIPs[0].String(), "."); idx != -1 { - s = localIPs[0].String()[:idx] - } - pkCmd.Flags().StringVarP(&ipAddr, "ip", "i", s+".2:7998", "ip:port to query") -} - -var pkCmd = &cobra.Command{ - Use: "pk", - Short: "Fetch Hypervisor Public Key", - Run: func(_ *cobra.Command, _ []string) { - s, err := visor.FetchHvPk(ipAddr) - if err != nil { - logger.WithError(err).Fatalln("failed to fetch hypervisor public key") - } - fmt.Printf("%s\n", s) - }, -} diff --git a/cmd/skywire-cli/commands/hv/hvskychat.go b/cmd/skywire-cli/commands/hv/hvskychat.go deleted file mode 100644 index f1a8c40e19..0000000000 --- a/cmd/skywire-cli/commands/hv/hvskychat.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build linux -// +build linux - -package clihv - -import ( - "github.com/spf13/cobra" - "github.com/toqueteos/webbrowser" -) - -func init() { - RootCmd.AddCommand(chatCmd) -} - -var chatCmd = &cobra.Command{ - Use: "skychat", - Short: "Skychat UI", - Run: func(_ *cobra.Command, _ []string) { - //TODO: get the actual port from config instead of using default value here - if err := webbrowser.Open("http://127.0.0.1:8001/"); err != nil { - logger.Fatal("Failed to open hypervisor UI in browser:", err) - } - }, -} diff --git a/cmd/skywire-cli/commands/hv/hvui.go b/cmd/skywire-cli/commands/hv/hvui.go deleted file mode 100644 index 6e27b64cd4..0000000000 --- a/cmd/skywire-cli/commands/hv/hvui.go +++ /dev/null @@ -1,21 +0,0 @@ -package clihv - -import ( - "github.com/spf13/cobra" - "github.com/toqueteos/webbrowser" -) - -func init() { - RootCmd.AddCommand(hvuiCmd) -} - -var hvuiCmd = &cobra.Command{ - Use: "ui", - Short: "Hypervisor UI", - Run: func(_ *cobra.Command, _ []string) { - //TODO: get the actual port from config instead of using default value here - if err := webbrowser.Open("http://127.0.0.1:8000/"); err != nil { - logger.Fatal("Failed to open hypervisor UI in browser:", err) - } - }, -} diff --git a/cmd/skywire-cli/commands/hv/hvvpn.go b/cmd/skywire-cli/commands/hv/hvvpn.go deleted file mode 100644 index 4a45d08b6e..0000000000 --- a/cmd/skywire-cli/commands/hv/hvvpn.go +++ /dev/null @@ -1,97 +0,0 @@ -package clihv - -import ( - "fmt" - "log" - - "github.com/spf13/cobra" - "github.com/toqueteos/webbrowser" - - "github.com/skycoin/skywire/pkg/visor/visorconfig" -) - -func init() { - RootCmd.AddCommand(vpnCmd) - vpnCmd.PersistentFlags().StringVarP(&rpcAddr, "rpc", "", "localhost:3435", "RPC server address") - vpnCmd.AddCommand( - vpnUICmd, - vpnURLCmd, - vpnListCmd, - ) - vpnUICmd.Flags().StringVarP(&path, "input", "i", "", "read from specified config file") - vpnUICmd.Flags().BoolVarP(&pkg, "pkg", "p", false, "read from /opt/skywire/skywire.json") - vpnURLCmd.Flags().StringVarP(&path, "input", "i", "", "read from specified config file") - vpnURLCmd.Flags().BoolVarP(&pkg, "pkg", "p", false, "read from /opt/skywire/skywire.json") -} - -var vpnCmd = &cobra.Command{ - Use: "vpn", - Short: "VPN UI", -} - -var vpnUICmd = &cobra.Command{ - Use: "ui", - Short: "Open VPN UI in default browser", - Run: func(_ *cobra.Command, _ []string) { - var url string - if pkg { - path = visorconfig.Pkgpath - } - if path != "" { - conf, err := visorconfig.ReadFile(path) - if err != nil { - log.Fatal("Failed to read in config:", err) - } - url = fmt.Sprintf("http://127.0.0.1:8000/#/vpn/%s/", conf.PK.Hex()) - } else { - client := rpcClient() - overview, err := client.Overview() - if err != nil { - log.Fatal("Failed to connect; is skywire running?\n", err) - } - url = fmt.Sprintf("http://127.0.0.1:8000/#/vpn/%s/", overview.PubKey.Hex()) - } - if err := webbrowser.Open(url); err != nil { - log.Fatal("Failed to open VPN UI in browser:", err) - } - }, -} - -var vpnURLCmd = &cobra.Command{ - Use: "url", - Short: "Show VPN UI URL", - Run: func(_ *cobra.Command, _ []string) { - var url string - if pkg { - path = visorconfig.Pkgpath - } - if path != "" { - conf, err := visorconfig.ReadFile(path) - if err != nil { - log.Fatal("Failed to read in config:", err) - } - url = fmt.Sprintf("http://127.0.0.1:8000/#/vpn/%s/", conf.PK.Hex()) - } else { - client := rpcClient() - overview, err := client.Overview() - if err != nil { - logger.Fatal("Failed to connect; is skywire running?\n", err) - } - url = fmt.Sprintf("http://127.0.0.1:8000/#/vpn/%s/", overview.PubKey.Hex()) - } - fmt.Println(url) - }, -} - -var vpnListCmd = &cobra.Command{ - Use: "list", - Short: "List public VPN servers", - Run: func(_ *cobra.Command, _ []string) { - client := rpcClient() - servers, err := client.VPNServers() - if err != nil { - logger.Fatal("Failed to connect; is skywire running?\n", err) - } - fmt.Println(servers) - }, -} diff --git a/cmd/skywire-cli/commands/hv/root.go b/cmd/skywire-cli/commands/hv/root.go deleted file mode 100644 index 6489688129..0000000000 --- a/cmd/skywire-cli/commands/hv/root.go +++ /dev/null @@ -1,38 +0,0 @@ -package clihv - -import ( - "net" - "time" - - "github.com/spf13/cobra" - - "github.com/skycoin/skywire-utilities/pkg/logging" - "github.com/skycoin/skywire/pkg/visor" -) - -var ( - logger = logging.MustGetLogger("skywire-cli") - rpcAddr string - path string - pk string - url string - pkg bool - ipAddr string - localIPs []net.IP - err error -) - -// RootCmd contains commands that interact with the skywire-visor -var RootCmd = &cobra.Command{ - Use: "hv", - Short: "Open HVUI in browser", -} - -func rpcClient() visor.API { - const rpcDialTimeout = time.Second * 5 - conn, err := net.DialTimeout("tcp", rpcAddr, rpcDialTimeout) - if err != nil { - logger.Fatal("RPC connection failed; is skywire running?\n", err) - } - return visor.NewRPCClient(logger, conn, visor.RPCPrefix, 0) -} diff --git a/cmd/skywire-cli/commands/mdisc/root.go b/cmd/skywire-cli/commands/mdisc/root.go index 1c03c82a09..6cfc58e6ef 100644 --- a/cmd/skywire-cli/commands/mdisc/root.go +++ b/cmd/skywire-cli/commands/mdisc/root.go @@ -22,6 +22,9 @@ var packageLogger = masterLogger.PackageLogger("mdisc:disc") func init() { RootCmd.PersistentFlags().StringVar(&mdAddr, "addr", utilenv.DmsgDiscAddr, "address of DMSG discovery server\n") + var helpflag bool + RootCmd.Flags().BoolVarP(&helpflag, "help", "h", false, "help for "+RootCmd.Use) + RootCmd.Flags().MarkHidden("help") //nolint } // RootCmd is the command that contains sub-commands which interacts with DMSG services. diff --git a/cmd/skywire-cli/commands/root.go b/cmd/skywire-cli/commands/root.go index 406694e8d5..ae61d88440 100644 --- a/cmd/skywire-cli/commands/root.go +++ b/cmd/skywire-cli/commands/root.go @@ -6,13 +6,15 @@ import ( cc "github.com/ivanpirog/coloredcobra" "github.com/spf13/cobra" + "github.com/skycoin/skywire-utilities/pkg/buildinfo" clicompletion "github.com/skycoin/skywire/cmd/skywire-cli/commands/completion" cliconfig "github.com/skycoin/skywire/cmd/skywire-cli/commands/config" clidmsgpty "github.com/skycoin/skywire/cmd/skywire-cli/commands/dmsgpty" - clihv "github.com/skycoin/skywire/cmd/skywire-cli/commands/hv" climdisc "github.com/skycoin/skywire/cmd/skywire-cli/commands/mdisc" + clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" clirtfind "github.com/skycoin/skywire/cmd/skywire-cli/commands/rtfind" clivisor "github.com/skycoin/skywire/cmd/skywire-cli/commands/visor" + clivpn "github.com/skycoin/skywire/cmd/skywire-cli/commands/vpn" ) var rootCmd = &cobra.Command{ @@ -25,6 +27,7 @@ var rootCmd = &cobra.Command{ SilenceErrors: true, SilenceUsage: true, DisableSuggestions: true, + Version: buildinfo.Version(), } func init() { @@ -32,11 +35,16 @@ func init() { cliconfig.RootCmd, clidmsgpty.RootCmd, clivisor.RootCmd, - clihv.RootCmd, + clivpn.RootCmd, clirtfind.RootCmd, climdisc.RootCmd, clicompletion.RootCmd, ) + var helpflag bool + rootCmd.PersistentFlags().StringVarP(&clirpc.Addr, "rpc", "", "localhost:3435", "RPC server address") + rootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for "+rootCmd.Use) + rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) + rootCmd.PersistentFlags().MarkHidden("help") //nolint } // Execute executes root CLI command. diff --git a/cmd/skywire-cli/commands/rpc/root.go b/cmd/skywire-cli/commands/rpc/root.go new file mode 100644 index 0000000000..76448e4ae3 --- /dev/null +++ b/cmd/skywire-cli/commands/rpc/root.go @@ -0,0 +1,25 @@ +package clirpc + +import ( + "net" + "time" + + "github.com/skycoin/skywire-utilities/pkg/logging" + "github.com/skycoin/skywire/pkg/visor" +) + +var ( + logger = logging.MustGetLogger("skywire-cli") + //Addr is the address (ip:port) of the rpc server + Addr string +) + +// Client is used by other skywire-cli commands to query the visor rpc +func Client() visor.API { + const rpcDialTimeout = time.Second * 5 + conn, err := net.DialTimeout("tcp", Addr, rpcDialTimeout) + if err != nil { + logger.Fatal("RPC connection failed; is skywire running?\n", err) + } + return visor.NewRPCClient(logger, conn, visor.RPCPrefix, 0) +} diff --git a/cmd/skywire-cli/commands/rtfind/root.go b/cmd/skywire-cli/commands/rtfind/root.go index 27fe815af9..a5ce9c20f2 100644 --- a/cmd/skywire-cli/commands/rtfind/root.go +++ b/cmd/skywire-cli/commands/rtfind/root.go @@ -24,6 +24,9 @@ func init() { RootCmd.Flags().Uint16VarP(&frMinHops, "min-hops", "n", 1, "minimum hops") RootCmd.Flags().Uint16VarP(&frMaxHops, "max-hops", "x", 1000, "maximum hops") RootCmd.Flags().DurationVarP(&timeout, "timeout", "t", 10*time.Second, "request timeout") + var helpflag bool + RootCmd.Flags().BoolVarP(&helpflag, "help", "h", false, "help for "+RootCmd.Use) + RootCmd.Flags().MarkHidden("help") //nolint } // RootCmd is the command that queries the route finder. diff --git a/cmd/skywire-cli/commands/visor/app.go b/cmd/skywire-cli/commands/visor/app.go index 54c4b90e8e..d63bdfc856 100644 --- a/cmd/skywire-cli/commands/visor/app.go +++ b/cmd/skywire-cli/commands/visor/app.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" + clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" "github.com/skycoin/skywire/cmd/skywire-cli/internal" "github.com/skycoin/skywire/pkg/app/appserver" ) @@ -34,7 +35,7 @@ var lsAppsCmd = &cobra.Command{ Use: "ls", Short: "List apps", Run: func(_ *cobra.Command, _ []string) { - states, err := rpcClient().Apps() + states, err := clirpc.Client().Apps() internal.Catch(err) w := tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', tabwriter.TabIndent) _, err = fmt.Fprintln(w, "app\tports\tauto_start\tstatus") @@ -59,7 +60,7 @@ var startAppCmd = &cobra.Command{ Short: "Launch app", Args: cobra.MinimumNArgs(1), Run: func(_ *cobra.Command, args []string) { - internal.Catch(rpcClient().StartApp(args[0])) + internal.Catch(clirpc.Client().StartApp(args[0])) fmt.Println("OK") }, } @@ -69,7 +70,7 @@ var stopAppCmd = &cobra.Command{ Short: "Halt app", Args: cobra.MinimumNArgs(1), Run: func(_ *cobra.Command, args []string) { - internal.Catch(rpcClient().StopApp(args[0])) + internal.Catch(clirpc.Client().StopApp(args[0])) fmt.Println("OK") }, } @@ -88,7 +89,7 @@ var setAppAutostartCmd = &cobra.Command{ default: internal.Catch(fmt.Errorf("invalid args[1] value: %s", args[1])) } - internal.Catch(rpcClient().SetAutoStart(args[0], autostart)) + internal.Catch(clirpc.Client().SetAutoStart(args[0], autostart)) fmt.Println("OK") }, } @@ -107,7 +108,7 @@ var appLogsSinceCmd = &cobra.Command{ t, err = time.Parse(time.RFC3339Nano, strTime) internal.Catch(err) } - logs, err := rpcClient().LogsSince(t, args[0]) + logs, err := clirpc.Client().LogsSince(t, args[0]) internal.Catch(err) if len(logs) > 0 { fmt.Println(logs) diff --git a/cmd/skywire-cli/commands/visor/exec.go b/cmd/skywire-cli/commands/visor/exec.go index b8e76133e1..c7f5397ca3 100644 --- a/cmd/skywire-cli/commands/visor/exec.go +++ b/cmd/skywire-cli/commands/visor/exec.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" + clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" "github.com/skycoin/skywire/cmd/skywire-cli/internal" ) @@ -20,7 +21,7 @@ var execCmd = &cobra.Command{ Short: "Execute a command", Args: cobra.MinimumNArgs(1), Run: func(_ *cobra.Command, args []string) { - out, err := rpcClient().Exec(strings.Join(args, " ")) + out, err := clirpc.Client().Exec(strings.Join(args, " ")) internal.Catch(err) fmt.Print(string(out)) }, diff --git a/cmd/skywire-cli/commands/visor/info.go b/cmd/skywire-cli/commands/visor/info.go index c2dfc4b3a4..4699933438 100644 --- a/cmd/skywire-cli/commands/visor/info.go +++ b/cmd/skywire-cli/commands/visor/info.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" + clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" "github.com/skycoin/skywire/pkg/visor/visorconfig" ) @@ -46,7 +47,7 @@ var pkCmd = &cobra.Command{ } fmt.Println(conf.PK.Hex()) } else { - client := rpcClient() + client := clirpc.Client() overview, err := client.Overview() if err != nil { logger.Fatal("Failed to connect:", err) @@ -76,7 +77,7 @@ var hvpkCmd = &cobra.Command{ } fmt.Println(conf.Hypervisors) } else { - client := rpcClient() + client := clirpc.Client() overview, err := client.Overview() if err != nil { logger.Fatal("Failed to connect:", err) @@ -90,7 +91,7 @@ var chvpkCmd = &cobra.Command{ Use: "chvpk", Short: "Public key of connected hypervisors", Run: func(_ *cobra.Command, _ []string) { - client := rpcClient() + client := clirpc.Client() overview, err := client.Overview() if err != nil { logger.Fatal("Failed to connect:", err) @@ -103,7 +104,7 @@ var summaryCmd = &cobra.Command{ Use: "info", Short: "Summary of visor info", Run: func(_ *cobra.Command, _ []string) { - summary, err := rpcClient().Summary() + summary, err := clirpc.Client().Summary() if err != nil { log.Fatal("Failed to connect:", err) } @@ -118,7 +119,7 @@ var buildInfoCmd = &cobra.Command{ Use: "version", Short: "Version and build info", Run: func(_ *cobra.Command, _ []string) { - client := rpcClient() + client := clirpc.Client() overview, err := client.Overview() if err != nil { log.Fatal("Failed to connect:", err) diff --git a/cmd/skywire-cli/commands/visor/root.go b/cmd/skywire-cli/commands/visor/root.go index 582b981ff8..2b4ad2571d 100644 --- a/cmd/skywire-cli/commands/visor/root.go +++ b/cmd/skywire-cli/commands/visor/root.go @@ -1,35 +1,14 @@ package clivisor import ( - "net" - "time" - "github.com/skycoin/skycoin/src/util/logging" "github.com/spf13/cobra" - - "github.com/skycoin/skywire/pkg/visor" ) var logger = logging.MustGetLogger("skywire-cli") -var rpcAddr string - -func init() { - RootCmd.PersistentFlags().StringVarP(&rpcAddr, "rpc", "", "localhost:3435", "RPC server address") -} - // RootCmd contains commands that interact with the skywire-visor var RootCmd = &cobra.Command{ Use: "visor", Short: "Query the Skywire Visor", } - -func rpcClient() visor.API { - const rpcDialTimeout = time.Second * 5 - - conn, err := net.DialTimeout("tcp", rpcAddr, rpcDialTimeout) - if err != nil { - logger.Fatal("RPC connection failed:", err) - } - return visor.NewRPCClient(logger, conn, visor.RPCPrefix, 0) -} diff --git a/cmd/skywire-cli/commands/visor/route.go b/cmd/skywire-cli/commands/visor/route.go index 0e1a49cc6a..2f4d421c30 100644 --- a/cmd/skywire-cli/commands/visor/route.go +++ b/cmd/skywire-cli/commands/visor/route.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" + clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" "github.com/skycoin/skywire/cmd/skywire-cli/internal" "github.com/skycoin/skywire/pkg/router" "github.com/skycoin/skywire/pkg/routing" @@ -35,7 +36,7 @@ var lsRulesCmd = &cobra.Command{ Use: "ls-rules", Short: "List routing rules", Run: func(_ *cobra.Command, _ []string) { - rules, err := rpcClient().RoutingRules() + rules, err := clirpc.Client().RoutingRules() internal.Catch(err) printRoutingRules(rules...) @@ -50,7 +51,7 @@ var ruleCmd = &cobra.Command{ id, err := strconv.ParseUint(args[0], 10, 32) internal.Catch(err) - rule, err := rpcClient().RoutingRule(routing.RouteID(id)) + rule, err := clirpc.Client().RoutingRule(routing.RouteID(id)) internal.Catch(err) printRoutingRules(rule) @@ -64,7 +65,7 @@ var rmRuleCmd = &cobra.Command{ Run: func(_ *cobra.Command, args []string) { id, err := strconv.ParseUint(args[0], 10, 32) internal.Catch(err) - internal.Catch(rpcClient().RemoveRoutingRule(routing.RouteID(id))) + internal.Catch(clirpc.Client().RemoveRoutingRule(routing.RouteID(id))) fmt.Println("OK") }, } @@ -119,7 +120,7 @@ var addRuleCmd = &cobra.Command{ rIDKey = rule.KeyRouteID() } - internal.Catch(rpcClient().SaveRoutingRule(rule)) + internal.Catch(clirpc.Client().SaveRoutingRule(rule)) fmt.Println("Routing Rule Key:", rIDKey) }, } diff --git a/cmd/skywire-cli/commands/visor/shutdown.go b/cmd/skywire-cli/commands/visor/shutdown.go index 38d0c24e4b..23a7b773e7 100644 --- a/cmd/skywire-cli/commands/visor/shutdown.go +++ b/cmd/skywire-cli/commands/visor/shutdown.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/spf13/cobra" + + clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" ) func init() { @@ -14,7 +16,7 @@ var shutdownCmd = &cobra.Command{ Use: "halt", Short: "Stop a running visor", Run: func(_ *cobra.Command, args []string) { - rpcClient().Shutdown() //nolint + clirpc.Client().Shutdown() //nolint fmt.Println("Visor was shut down") }, } diff --git a/cmd/skywire-cli/commands/visor/transports.go b/cmd/skywire-cli/commands/visor/transports.go index 6790ecd241..1d18d3175a 100644 --- a/cmd/skywire-cli/commands/visor/transports.go +++ b/cmd/skywire-cli/commands/visor/transports.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" "github.com/skycoin/skywire-utilities/pkg/cipher" + clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" "github.com/skycoin/skywire/cmd/skywire-cli/internal" "github.com/skycoin/skywire/pkg/transport" "github.com/skycoin/skywire/pkg/transport/network" @@ -45,13 +46,20 @@ func init() { var tpCmd = &cobra.Command{ Use: "tp", Short: "View and set transports", + Long: ` + Transports are bidirectional communication protocols + used between two Skywire Visors (or Transport Edges) + + Each Transport is represented as a unique 16 byte (128 bit) + UUID value called the Transport ID + and has a Transport Type that identifies + a specific implementation of the Transport.`, } var lsTypesCmd = &cobra.Command{ - Use: "type", - Short: "Transport types used by the local visor", + Use: "type", Short: "Transport types used by the local visor", Run: func(_ *cobra.Command, _ []string) { - types, err := rpcClient().TransportTypes() + types, err := clirpc.Client().TransportTypes() internal.Catch(err) for _, t := range types { fmt.Println(t) @@ -60,7 +68,7 @@ var lsTypesCmd = &cobra.Command{ } func init() { - lsTpCmd.Flags().StringSliceVarP(&filterTypes, "types", "t", filterTypes, "show transport(s) type(s) comma-separated") + lsTpCmd.Flags().StringSliceVarP(&filterTypes, "types", "t", filterTypes, "show transport(s) type(s) comma-separated\n") lsTpCmd.Flags().VarP(&filterPubKeys, "pks", "p", "show transport(s) for public key(s) comma-separated") lsTpCmd.Flags().BoolVarP(&showLogs, "logs", "l", true, "show transport logs") } @@ -69,9 +77,9 @@ var lsTpCmd = &cobra.Command{ Use: "ls", Short: "Available transports", Run: func(_ *cobra.Command, _ []string) { - transports, err := rpcClient().Transports(filterTypes, filterPubKeys, showLogs) + transports, err := clirpc.Client().Transports(filterTypes, filterPubKeys, showLogs) internal.Catch(err) - printTransports(transports...) + PrintTransports(transports...) }, } @@ -81,9 +89,9 @@ var idCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Run: func(_ *cobra.Command, args []string) { tpID := internal.ParseUUID("transport-id", args[0]) - tp, err := rpcClient().Transport(tpID) + tp, err := clirpc.Client().Transport(tpID) internal.Catch(err) - printTransports(tp) + PrintTransports(tp) }, } @@ -117,7 +125,7 @@ var addTpCmd = &cobra.Command{ var err error if transportType != "" { - tp, err = rpcClient().AddTransport(pk, transportType, timeout) + tp, err = clirpc.Client().AddTransport(pk, transportType, timeout) if err != nil { logger.WithError(err).Fatalf("Failed to establish %v transport", transportType) } @@ -131,7 +139,7 @@ var addTpCmd = &cobra.Command{ network.DMSG, } for _, transportType := range transportTypes { - tp, err = rpcClient().AddTransport(pk, string(transportType), timeout) + tp, err = clirpc.Client().AddTransport(pk, string(transportType), timeout) if err == nil { logger.Infof("Established %v transport to %v", transportType, pk) break @@ -139,7 +147,7 @@ var addTpCmd = &cobra.Command{ logger.WithError(err).Warnf("Failed to establish %v transport", transportType) } } - printTransports(tp) + PrintTransports(tp) }, } @@ -149,12 +157,13 @@ var rmTpCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Run: func(_ *cobra.Command, args []string) { tID := internal.ParseUUID("transport-id", args[0]) - internal.Catch(rpcClient().RemoveTransport(tID)) + internal.Catch(clirpc.Client().RemoveTransport(tID)) fmt.Println("OK") }, } -func printTransports(tps ...*visor.TransportSummary) { +// PrintTransports prints transports used by the visor +func PrintTransports(tps ...*visor.TransportSummary) { sortTransports(tps...) w := tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', tabwriter.TabIndent) _, err := fmt.Fprintln(w, "type\tid\tremote\tmode\tlabel") @@ -194,19 +203,20 @@ var discTpCmd = &cobra.Command{ }, Run: func(_ *cobra.Command, _ []string) { - if rc := rpcClient(); tpPK.Null() { + if rc := clirpc.Client(); tpPK.Null() { entry, err := rc.DiscoverTransportByID(uuid.UUID(tpID)) internal.Catch(err) - printTransportEntries(entry) + PrintTransportEntries(entry) } else { entries, err := rc.DiscoverTransportsByPK(tpPK) internal.Catch(err) - printTransportEntries(entries...) + PrintTransportEntries(entries...) } }, } -func printTransportEntries(entries ...*transport.Entry) { +// PrintTransportEntries prints the transport entries +func PrintTransportEntries(entries ...*transport.Entry) { w := tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', tabwriter.TabIndent) _, err := fmt.Fprintln(w, "id\ttype\tedge1\tedge2") internal.Catch(err) diff --git a/cmd/skywire-cli/commands/vpn/root.go b/cmd/skywire-cli/commands/vpn/root.go new file mode 100644 index 0000000000..9778d6ce91 --- /dev/null +++ b/cmd/skywire-cli/commands/vpn/root.go @@ -0,0 +1,24 @@ +package clivpn + +import ( + "github.com/spf13/cobra" + + "github.com/skycoin/skywire-utilities/pkg/logging" +) + +var ( + logger = logging.MustGetLogger("skywire-cli") + path string + isPkg bool + isUnFiltered bool + ver string + country string + isSystray bool + isStats bool +) + +// RootCmd contains commands that interact with the skywire-visor +var RootCmd = &cobra.Command{ + Use: "vpn", + Short: "controls for VPN client", +} diff --git a/cmd/skywire-cli/commands/vpn/vvpn.go b/cmd/skywire-cli/commands/vpn/vvpn.go new file mode 100644 index 0000000000..8d65268140 --- /dev/null +++ b/cmd/skywire-cli/commands/vpn/vvpn.go @@ -0,0 +1,209 @@ +package clivpn + +import ( + "encoding/json" + "fmt" + "log" + "os" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + "github.com/toqueteos/webbrowser" + + "github.com/skycoin/skywire-utilities/pkg/buildinfo" + clirpc "github.com/skycoin/skywire/cmd/skywire-cli/commands/rpc" + "github.com/skycoin/skywire/cmd/skywire-cli/internal" + "github.com/skycoin/skywire/pkg/app/appserver" + "github.com/skycoin/skywire/pkg/servicedisc" + "github.com/skycoin/skywire/pkg/visor/visorconfig" +) + +func init() { + RootCmd.AddCommand( + vpnListCmd, + vpnUICmd, + vpnURLCmd, + vpnStartCmd, + vpnStopCmd, + vpnStatusCmd, + ) + version := buildinfo.Version() + if version == "unknown" { + version = "" + } + vpnUICmd.Flags().BoolVarP(&isPkg, "pkg", "p", false, "use package config path") + vpnUICmd.Flags().StringVarP(&path, "config", "c", "", "config path") + vpnListCmd.Flags().BoolVarP(&isUnFiltered, "nofilter", "n", false, "provide unfiltered results") + vpnListCmd.Flags().StringVarP(&ver, "ver", "v", version, "filter results by version") + vpnListCmd.Flags().StringVarP(&country, "country", "c", "", "filter results by country") + vpnListCmd.Flags().BoolVarP(&isStats, "stats", "s", false, "return only a count of the resuts") + vpnListCmd.Flags().BoolVarP(&isSystray, "systray", "y", false, "format results for isSystray") +} + +var vpnUICmd = &cobra.Command{ + Use: "ui", + Short: "Open VPN UI in default browser", + Run: func(_ *cobra.Command, _ []string) { + var url string + if isPkg { + path = visorconfig.Pkgpath + } + if path != "" { + conf, err := visorconfig.ReadFile(path) + if err != nil { + log.Fatal("Failed to read in config:", err) + } + url = fmt.Sprintf("http://127.0.0.1:8000/#/vpn/%s/", conf.PK.Hex()) + } else { + client := clirpc.Client() + overview, err := client.Overview() + if err != nil { + log.Fatal("Failed to connect; is skywire running?\n", err) + } + url = fmt.Sprintf("http://127.0.0.1:8000/#/vpn/%s/", overview.PubKey.Hex()) + } + if err := webbrowser.Open(url); err != nil { + log.Fatal("Failed to open VPN UI in browser:", err) + } + }, +} + +var vpnURLCmd = &cobra.Command{ + Use: "url", + Short: "Show VPN UI URL", + Run: func(_ *cobra.Command, _ []string) { + var url string + if isPkg { + path = visorconfig.Pkgpath + } + if path != "" { + conf, err := visorconfig.ReadFile(path) + if err != nil { + log.Fatal("Failed to read in config:", err) + } + url = fmt.Sprintf("http://127.0.0.1:8000/#/vpn/%s/", conf.PK.Hex()) + } else { + client := clirpc.Client() + overview, err := client.Overview() + if err != nil { + logger.Fatal("Failed to connect; is skywire running?\n", err) + } + url = fmt.Sprintf("http://127.0.0.1:8000/#/vpn/%s/", overview.PubKey.Hex()) + } + fmt.Println(url) + }, +} + +var vpnListCmd = &cobra.Command{ + Use: "list", + Short: "List public VPN servers", + Run: func(_ *cobra.Command, _ []string) { + client := clirpc.Client() + if isUnFiltered { + ver = "" + country = "" + } + // servers, err := client.VPNServers(ver, country) //query filtering + servers, err := client.VPNServers() + if err != nil { + logger.Fatal("Failed to connect; is skywire running?\n", err) + } + + /*vv remove when query filtering is implemented vv*/ + var a []servicedisc.Service + for _, i := range servers { + if (ver == "") || (ver == "unknown") || (strings.Replace(i.Version, "v", "", 1) == ver) { + a = append(a, i) + } + } + if len(a) > 0 { + servers = a + a = []servicedisc.Service{} + } + if country != "" { + for _, i := range servers { + if i.Geo != nil { + if i.Geo.Country == country { + a = append(a, i) + } + } + } + servers = a + } + /*^^ remove when query filtering is implemented ^^*/ + + if len(servers) == 0 { + fmt.Printf("No VPN Servers found\n") + os.Exit(0) + } + if isStats { + fmt.Printf("%d VPN Servers\n", len(servers)) + os.Exit(0) + } + if isSystray { + for _, i := range servers { + b := strings.Replace(i.Addr.String(), ":44", "", 1) + fmt.Printf("%s", b) + if i.Geo != nil { + fmt.Printf(" | ") + fmt.Printf("%s\n", i.Geo.Country) + } else { + fmt.Printf("\n") + } + } + os.Exit(0) + } + j, err := json.MarshalIndent(servers, "", "\t") + if err != nil { + logger.WithError(err).Fatal("Could not marshal json.") + } + + fmt.Printf("%s", j) + }, +} + +var vpnStartCmd = &cobra.Command{ + Use: "start", + Short: "start the vpn for ", + Args: cobra.MinimumNArgs(1), + Run: func(_ *cobra.Command, args []string) { + fmt.Println("%s", args[0]) + internal.Catch(clirpc.Client().StartVPNClient(args[0])) + fmt.Println("OK") + }, +} + +var vpnStopCmd = &cobra.Command{ + Use: "stop", + Short: "stop the vpn", + Run: func(_ *cobra.Command, _ []string) { + internal.Catch(clirpc.Client().StopVPNClient("vpn-client")) + fmt.Println("OK") + }, +} + +var vpnStatusCmd = &cobra.Command{ + Use: "status", + Short: "vpn status", + Run: func(_ *cobra.Command, _ []string) { + states, err := clirpc.Client().Apps() + internal.Catch(err) + w := tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', tabwriter.TabIndent) + internal.Catch(err) + for _, state := range states { + if state.Name == "vpn-client" { + status := "stopped" + if state.Status == appserver.AppStatusRunning { + status = "running" + } + if state.Status == appserver.AppStatusErrored { + status = "errored" + } + _, err = fmt.Fprintf(w, "%s\n", status) + internal.Catch(err) + } + } + internal.Catch(w.Flush()) + }, +} diff --git a/cmd/skywire-systray/icons/icon.ico b/cmd/skywire-systray/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a59afb5cfbe34bbdf93a60276e440f2034f429bd GIT binary patch literal 67646 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 literal 0 HcmV?d00001 diff --git a/cmd/skywire-systray/icons/icon.png b/cmd/skywire-systray/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fddafbe9ac242a663c19f319206652daad355cc9 GIT binary patch literal 7025 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 literal 0 HcmV?d00001 diff --git a/cmd/skywire-systray/icons/icon.tiff b/cmd/skywire-systray/icons/icon.tiff new file mode 100644 index 0000000000000000000000000000000000000000..b635a4967c9ff9f7cf48f30580dd9579156d7409 GIT binary patch literal 35948 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 q)qefBU{zI#0U)VZ*>~qJl zJNFCa^R90qHD89#UT1G<+ezfF@p5Vr=KIOgRhFnXWfnYqE!&IwDa}Y|B4e0!Cy;Z{A4QOFlZBznnj7 zl~_NRhlfuLbT9{1xK({fRXKDzqwyU0-KAL2vcthdekJnezGfa_*G0C&cc}hcEL``8 zQ*_+5M1HrqJCxd9wfAvSOqa;yy?p{_bb!U&4>hI7Fy}HJ+rd^|usEL{9FfH(=?gSY zMaCqHsOd`No^(9fiuhtAdP^E9R+Z+=`VzK>4SvL_cll0?y|G^cy|QzMj6YZG13sRl z8aClI25&u}c;As;aU6szDauo3Xs*S(Hpf;Df&+)A+>$8WJPzkvr=)_W_ zO5|!U6Tw&GJ(zaSAkJOMfDFdRw|q%2B-}#3pk*kBsydZHyhH0v0vR8%%q2m zgvb@?XRUC}l(B!08ZMV=v8Ou_Qz<;{>6IuQ!l-`Nl)8xG0WS1mWPj8>Z8o{*$)kZB zy_sv${9_f-rS8F%Y$=X{Y4dTZuPyrOQ)w3YHeb-bU^aLSQSmmO%CZn)9r{%uCzQ@C z_tTOq`4_z+_sr$24uMBQQ)gSA0oiv(ZY5jrG8=l2(-LJL7SMIkYUOj2J>M@>x8|_< zB^)T1AKG<&G}3;%xb{PyJWK4bQL@#tbT4u2QONM?7^(FFnxeFj((x>DwcOvVBLr=5QIrah`MVLH7cvguvkajR~4y6V2a7j+;dH2+z%l&PPsab@SDcax_GmlAn) z@moyKN0g3SS9K}~-mzUKZ@Dme<@l&KBAwPE1y*A1D^&{mL)t%NeFFJLYewy~So+L; z#n~0=Z}OWv{L1zBRDbm*Eqrj=F)V&_;B7-kc;U=xRL~ZGXXE*-0FE=sA;VD(P0!T9 z%Wq$WZ550+j*BeJd5DKpjL*HDe6-`j*?DK1|J%FC7yB;R}vMGqwR0MKTQ4wT)7a~9RUPs>LT1VhdF%5o4R#M6XnLmAGuS=+%|QU$}LAe zdgnWL`_yAPcP>5rt^j|>)JrD!9NXAkZ}D^SFPjIKSNv|FeAm<$E{~j>Pj|nof1LUY zdU8c2?Fs00f4<1)nUkKf=WW{aSyRZ1s~~N!(6;yURWYxe`i#Br&V8RBOL0J<|INt% z2krd782SI%|KE)K$E%S4cSim>9UY7MpNxDc06ad1{J%5u|2h7A8c+c7&!wF2PyII| z|M9~Aoso}a`oA;svA};b^09p9=lN$zI$!^}<^THrUyOVJd<+26JOCiB005@-=STeK z^A9s0Epz8T*!+K~`TtV$|9OA_;9qLK_J92l|JnXa&HtC0|1UNFUuyop)ck*``TtV$ P|E1>tOU?iPqvrn)@x`mk literal 0 HcmV?d00001 diff --git a/cmd/skywire-systray/skywire-systray.go b/cmd/skywire-systray/skywire-systray.go new file mode 100644 index 0000000000..4d78d90fd2 --- /dev/null +++ b/cmd/skywire-systray/skywire-systray.go @@ -0,0 +1,430 @@ +/* +skywire systray +*/ +package main + +import ( + "embed" + "fmt" + "log" + "strings" + "sync" + "time" + + "github.com/bitfield/script" + cc "github.com/ivanpirog/coloredcobra" + "github.com/skycoin/skycoin/src/util/logging" + "github.com/skycoin/systray" + "github.com/spf13/cobra" + + "github.com/skycoin/skywire/pkg/skyenv" +) + +var ( + isSourcerun bool + isDevrun bool + remotevisors []string + vpnserverpks []string + skywirecli string + mHV *systray.MenuItem + mVisors *systray.MenuItem + mVPN *systray.MenuItem + mVPNButton *systray.MenuItem + mVPNClient *systray.MenuItem + mVPNStatus *systray.MenuItem + mVPNUI *systray.MenuItem //nolint:unused + mPTY *systray.MenuItem + mShutdown *systray.MenuItem + mStart *systray.MenuItem + mAutoconfig *systray.MenuItem + mQuit *systray.MenuItem + mRemoteVisors []*systray.MenuItem + mVPNServers []*systray.MenuItem + servers []*systray.MenuItem //nolint + l *logging.MasterLogger + vpnStatusMx sync.Mutex + err error +) + +func init() { + l = logging.NewMasterLogger() + //disable sorting, flags appear in the order shown here + rootCmd.Flags().SortFlags = false + rootCmd.Flags().BoolVarP(&isSourcerun, "src", "s", false, "'go run' using the skywire sources") + rootCmd.Flags().BoolVarP(&isDevrun, "dev", "d", false, "show remote visors & dmsghttp ui") + +} + +var rootCmd = &cobra.Command{ + Use: "skywire-systray", + Short: "skywire systray", + SilenceErrors: true, + SilenceUsage: true, + DisableSuggestions: true, + // PreRun: func(cmd *cobra.Command, _ []string) { + // }, + Run: func(cmd *cobra.Command, args []string) { + //skywire-cli command to use + if !isSourcerun { + skywirecli = "skywire-cli" + } else { + skywirecli = "go run cmd/skywire-cli/skywire-cli.go" + } + onExit := func() { + now := time.Now() + fmt.Println("Exit at", now.String()) + } + systray.Run(onReady, onExit) + }, +} + +// Execute executes root command. +func Execute() { + cc.Init(&cc.Config{ + RootCmd: rootCmd, + Headings: cc.HiBlue + cc.Bold, //+ cc.Underline, + Commands: cc.HiBlue + cc.Bold, + CmdShortDescr: cc.HiBlue, + Example: cc.HiBlue + cc.Italic, + ExecName: cc.HiBlue + cc.Bold, + Flags: cc.HiBlue + cc.Bold, + //FlagsDataType: cc.HiBlue, + FlagsDescr: cc.HiBlue, + NoExtraNewlines: true, + NoBottomNewline: true, + }) + if err = rootCmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} + +//go:embed icons/* +var iconFS embed.FS + +func main() { + Execute() +} + +func onReady() { + l := logging.NewMasterLogger() + sysTrayIcon, err := ReadSysTrayIcon() + if err != nil { + l.WithError(err).Fatalln("Failed to read system tray icon") + } + systray.SetTemplateIcon(sysTrayIcon, sysTrayIcon) + systray.SetTitle("Skywire") + systray.SetTooltip("Skywire") + mQuit = systray.AddMenuItem("Quit", "Quit the whole app") + + //check that the visor is running and responds over RPC + visor, err := script.Exec(skywirecli + ` visor pk`).Match("FATAL").String() + if err != nil { + l.WithError(err).Warn("Failed to get visor public key") + //visor should be empty string if the visor is running + visor = " " + } + systray.SetTemplateIcon(sysTrayIcon, sysTrayIcon) + systray.SetTitle("Skywire") + + //Top level menu + //mHV launches the hypervisor with `skywire-cli hv ui` + mHV = systray.AddMenuItem("Hypervisor", "Hypervisor") + mHV.Hide() + //mPTY launches the dmsgpty ui with `skywire-cli hv dmsg ui` + mPTY = systray.AddMenuItem("DMSGPTY UI", "DMSGPTY UI") + mPTY.Hide() + //mVPNUI launches the VPN ui with `skywire-cli hv dmsg ui` + mVPNUI = systray.AddMenuItem("VPN UI", "VPN UI") + mVPNUI.Hide() + //mVisors menu to access dmsgpty ui for connected remote visors + mVisors = systray.AddMenuItem("Visors", "Visors") + mVisors.Hide() + //mVPNClient contains the vpn menu and server list submenu + mVPNClient = systray.AddMenuItem("VPN", "VPN Client Submenu") + mVPNClient.Hide() + //mStart start a stopped the visor + mStart = systray.AddMenuItem("Start", "Start") + mStart.Hide() + //mAutoconfig run the autoconfig script provided by the package or installer + mAutoconfig = systray.AddMenuItem("Autoconfig", "Autoconfig") + mAutoconfig.Hide() + //mShutdown shut down a running visor + mShutdown = systray.AddMenuItem("Shutdown", "Shutdown") + mShutdown.Hide() + + //Sub menus + //mVPNStatus shows current VPN connection status derived from `skywire-cli visor app info` + mVPNStatus = mVPNClient.AddSubMenuItem("Status: Disconnected", "VPN Client Status") + mVPNStatus.Disable() + //mVPNButton VPN on / off button + mVPNButton = mVPNClient.AddSubMenuItem("Connect", "VPN Client Switch Button") + //mVPN is the list of VPN server public keys returned by `skywire-cli hv vpn list` + mVPN = mVPNClient.AddSubMenuItem("VPN Servers", "VPN Servers") + + if visor != "" { + ToggleOff() + } else { + if isDevrun { + //check for connected visors + visors, err := script.Exec(skywirecli + ` dmsgpty list`).String() + if err != nil { + l.WithError(err).Warn("Failed to fetch connected visors " + visors) + } + remotevisors = strings.Split(visors, "\n") + for i := range remotevisors { + if remotevisors[i] != "" { + l.Info("remote visors: " + remotevisors[i]) + } + } + mRemoteVisors = []*systray.MenuItem{} + for _, v := range remotevisors { + if v != "" { + mRemoteVisors = append(mRemoteVisors, mVisors.AddSubMenuItem(v, "")) + } + } + go visorsBtn(mRemoteVisors) + } + go vpnStatusBtn() + //check for available vpn servers + vpnlistpks, err := script.Exec(skywirecli + ` vpn list -y`).String() + if err != nil { + l.WithError(err).Warn("Failed to fetch vpn servers") + } + vpnlistpks = strings.Trim(vpnlistpks, "[") + vpnlistpks = strings.Trim(vpnlistpks, "]") + vpnserverpks = strings.Split(vpnlistpks, "\n") + mVPNServers = []*systray.MenuItem{} + for _, v := range vpnserverpks { + if v != "" { + mVPNServers = append(mVPNServers, mVPN.AddSubMenuItemCheckbox(v, "", false)) + } + } + go serversBtn(mVPNServers) + ToggleOn() + } + systray.AddSeparator() + //this blank item retains minimum text displacement + + go func() { + <-mQuit.ClickedCh + fmt.Println("Requesting quit") + systray.Quit() + fmt.Println("Finished quitting") + }() + go func() { + for { + select { + case <-mHV.ClickedCh: + _, err = script.Exec(skywirecli + ` visor hvui`).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to open hypervisor UI") + } + case <-mVPNUI.ClickedCh: + _, err = script.Exec(skywirecli + ` vpn ui`).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to open VPN UI") + } + case <-mVPNButton.ClickedCh: + handleVPNButton() + case <-mPTY.ClickedCh: + _, err = script.Exec(skywirecli + ` dmsg ui`).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to open dmsgpty UI") + } + case <-mStart.ClickedCh: + _, err = script.Exec(`systemctl enable --now skywire`).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to start skywire") + } else { + ToggleOn() + } + case <-mAutoconfig.ClickedCh: + //execute the skywire-sudoautoconfig script includedwith the skywire package + _, err = script.Exec(`exo-open --launch TerminalEmulator bash -c 'sudo SKYBIAN=true skywire-autoconfig && sleep 5'`).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to generate skywire configuration") + } else { + ToggleOn() + } + case <-mShutdown.ClickedCh: + if skyenv.OS == "linux" { + _, _ = script.Exec(`systemctl disable --now skywire`).Stdout() //nolint:errcheck + ToggleOff() + } else { + l.Warn("shutdown of services not yet implemented on windows / mac") + } + _, err = script.Exec(skywirecli + ` visor halt 2> /dev/null`).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to stop skywire") + } else { + ToggleOff() + } + case <-mQuit.ClickedCh: + systray.Quit() + fmt.Println("Quit2 now...") + return + } + } + }() +} + +// ReadSysTrayIcon reads system tray icon. +func ReadSysTrayIcon() (contents []byte, err error) { + contents, err = iconFS.ReadFile("icons/icon.png") + if err != nil { + err = fmt.Errorf("failed to read icon: %w", err) + } + return contents, err +} + +func visorsBtn(mRemoteVisors []*systray.MenuItem) { + btnChannel := make(chan int) + for index, remotevisor := range mRemoteVisors { + go func(chn chan int, remotevisor *systray.MenuItem, index int) { + for { //nolint + select { + case <-remotevisor.ClickedCh: + l.Info("opening dmsgpty ui to visor: " + remotevisors[index]) + _, err = script.Exec(skywirecli + ` hv dmsg ui -v ` + remotevisors[index]).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to open dmsgpty UI") + } + chn <- index + } + } + }(btnChannel, remotevisor, index) + } +} + +func serversBtn(servers []*systray.MenuItem) { //nolint + btnChannel := make(chan int) + for index, server := range servers { //nolint + go func(chn chan int, server *systray.MenuItem, index int) { + for { //nolint + select { + case <-server.ClickedCh: + chn <- index + } + } + }(btnChannel, server, index) + } + + for { + selectedServer := servers[<-btnChannel] + serverTempValue := strings.Split(selectedServer.String(), ",")[2] + serverPK := serverTempValue[2 : len(serverTempValue)-7] + for _, server := range servers { //nolint + server.Uncheck() + server.Enable() + } + selectedServer.Check() + selectedServer.Disable() + // pk := cipher.PubKey{} + // if err := pk.UnmarshalText([]byte(serverPK)); err != nil { + // continue + // } + stats, err := script.Exec(skywirecli + ` vpn status`).String() + if err != nil { + break + } + if stats == "running\n" { + _, err = script.Exec(skywirecli + ` vpn stop`).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to stop vpn-client") + } + } + _, err = script.Exec(`bash -c 'export VPNSERVERPK=` + serverPK + ` ; ` + skywirecli + ` vpn start ${VPNSERVERPK%% *}'`).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to start vpn-client") + } + } +} + +func vpnStatusBtn() { + for { + vpnStatusMx.Lock() + stats, err := script.Exec(skywirecli + ` vpn status`).String() + if err != nil { + mVPNStatus.SetTitle("Status: Disconnected") + mVPNButton.SetTitle("Connect") + break + } + if stats == "running\n" { + mVPNStatus.SetTitle("Status: Connected") + mVPNButton.SetTitle("Disconnect") + } + if stats == "stopped\n" { + mVPNStatus.SetTitle("Status: Disconnected") + mVPNButton.SetTitle("Connect") + } + if stats == "error\n" { + mVPNStatus.SetTitle("Status: Error") + mVPNButton.SetTitle("Connect") + + } + vpnStatusMx.Unlock() + time.Sleep(2 * time.Second) + } +} + +func handleVPNButton() { //nolint + appstate, err := script.Exec(skywirecli + ` vpn status`).String() + if err != nil { + l.WithError(err).Warn("Failed to get vpn-client status") + } + if appstate == "running\n" { + _, err = script.Exec(skywirecli + ` vpn stop `).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to stop vpn-client") + } + } else { + _, err = script.Exec(skywirecli + ` vpn start `).Stdout() + if err != nil { + l.WithError(err).Warn("Failed to start vpn-client") + } + } +} + +// ToggleOn menu when skywire visor is running +func ToggleOn() { + //check for connected visors + visors, err := script.Exec(skywirecli + ` dmsgpty list`).String() + if err != nil { + l.WithError(err).Warn("Failed to fetch connected visors " + visors) + } + if isDevrun { + mPTY.Show() + if (visors != "") && (visors != "\n") { + mVisors.Show() + } else { + mVisors.Hide() + } + } else { + mVisors.Hide() + mPTY.Hide() + } + mHV.Show() + mVPNUI.Show() + mVPN.Show() + mVPNClient.Show() + mStart.Hide() + mAutoconfig.Hide() + mShutdown.Show() + mQuit.Show() +} + +// ToggleOff menu when skywire visor is NOT running +func ToggleOff() { + mHV.Hide() + mPTY.Hide() + mVPNUI.Hide() + mVPNClient.Hide() + mVisors.Hide() + mShutdown.Hide() + + mStart.Show() + if skyenv.OS == "linux" { + mAutoconfig.Show() + } + mQuit.Show() +} diff --git a/pkg/servicedisc/autoconnect.go b/pkg/servicedisc/autoconnect.go index 15576435a1..4761c8e20a 100644 --- a/pkg/servicedisc/autoconnect.go +++ b/pkg/servicedisc/autoconnect.go @@ -113,6 +113,7 @@ func (a *autoconnector) fetchPubAddresses(ctx context.Context) ([]cipher.PubKey, var services []Service fetch := func() (err error) { // "return" services up from the closure + //services, err = a.client.Services(ctx, a.maxConns, "", "") //query filtering services, err = a.client.Services(ctx, a.maxConns) if err != nil { return err diff --git a/pkg/servicedisc/client.go b/pkg/servicedisc/client.go index 19a4132059..d54986fc1d 100644 --- a/pkg/servicedisc/client.go +++ b/pkg/servicedisc/client.go @@ -29,6 +29,9 @@ const ( updateRetryDelay = 5 * time.Second discServiceTypeParam = "type" discServiceQtyParam = "quantity" + +// discServiceCountryParam = "country" //query filtering +// discServiceVersionParam = "version" //query filtering ) // Config configures the HTTPClient. @@ -68,6 +71,7 @@ func NewClient(log logrus.FieldLogger, mLog *logging.MasterLogger, conf Config, } func (c *HTTPClient) addr(path, serviceType string, quantity int) (string, error) { + //func (c *HTTPClient) addr(path, serviceType, version, country string, quantity int) (string, error) { //query filtering addr := c.conf.DiscAddr url, err := url.Parse(addr) if err != nil { @@ -81,6 +85,15 @@ func (c *HTTPClient) addr(path, serviceType string, quantity int) (string, error if quantity > 1 { q.Set(discServiceQtyParam, strconv.Itoa(quantity)) } + //query filtering + /* + if version != "" { + q.Set(discServiceVersionParam, version) + } + if country != "" { + q.Set(discServiceCountryParam, country) + } + */ url.RawQuery = q.Encode() return url.String(), nil } @@ -112,6 +125,8 @@ func (c *HTTPClient) Auth(ctx context.Context) (*httpauth.Client, error) { // Services calls 'GET /api/services'. func (c *HTTPClient) Services(ctx context.Context, quantity int) (out []Service, err error) { + //func (c *HTTPClient) Services(ctx context.Context, quantity int, version, country string) (out []Service, err error) { //query filtering + //url, err := c.addr("/api/services", c.entry.Type, version, country, quantity) url, err := c.addr("/api/services", c.entry.Type, quantity) if err != nil { return nil, err @@ -187,6 +202,7 @@ func (c *HTTPClient) postEntry(ctx context.Context) (Service, error) { return Service{}, err } + // url, err := c.addr("/api/services", "", "", "", 1) //query filtering url, err := c.addr("/api/services", "", 1) if err != nil { return Service{}, nil @@ -244,6 +260,7 @@ func (c *HTTPClient) DeleteEntry(ctx context.Context) (err error) { return err } + // url, err := c.addr("/api/services/"+c.entry.Addr.String(), c.entry.Type, "", "", 1) //query filtering url, err := c.addr("/api/services/"+c.entry.Addr.String(), c.entry.Type, 1) if err != nil { return err diff --git a/pkg/visor/api.go b/pkg/visor/api.go index 799485e3b3..6d843b2984 100644 --- a/pkg/visor/api.go +++ b/pkg/visor/api.go @@ -36,11 +36,12 @@ type API interface { Health() (*HealthInfo, error) Uptime() (float64, error) - App(appName string) (*appserver.AppState, error) Apps() ([]*appserver.AppState, error) StartApp(appName string) error StopApp(appName string) error + StartVPNClient(pubkey string) error + StopVPNClient(appName string) error SetAppDetailedStatus(appName, state string) error SetAppError(appName, stateErr string) error RestartApp(appName string) error @@ -54,7 +55,8 @@ type API interface { GetAppStats(appName string) (appserver.AppStats, error) GetAppError(appName string) (string, error) GetAppConnectionsSummary(appName string) ([]appserver.ConnectionSummary, error) - VPNServers() ([]string, error) + // VPNServers(version, country string) ([]servicedisc.Service, error) //query filtering + VPNServers() ([]servicedisc.Service, error) RemoteVisors() ([]string, error) TransportTypes() ([]string, error) @@ -360,6 +362,52 @@ func (v *Visor) StopApp(appName string) error { return ErrProcNotAvailable } +// StartVPNClient implements API. +func (v *Visor) StartVPNClient(pubkey string) error { + var envs []string + var err error + if v.tpM == nil { + return ErrTrpMangerNotAvailable + } + if len(v.conf.Launcher.Apps) > 0 { + v.conf.Launcher.Apps[0].Args = []string{"-srv", pubkey} + } else { + return errors.New("no vpn app configuration found") + } + maker := vpnEnvMaker(v.conf, v.dmsgC, v.dmsgDC, v.tpM.STCPRRemoteAddrs()) + envs, err = maker() + if err != nil { + return err + } + + if v.GetVPNClientAddress() == "" { + return errors.New("VPN server pub key is missing") + } + var pk cipher.PubKey + err = pk.Set(pubkey) + if err != nil { + return err + } + + getRouteSetupHooks(context.Background(), v, v.log) + // check process manager availability + if v.procM != nil { + return v.appL.StartApp(skyenv.VPNClientName, v.conf.Launcher.Apps[0].Args, envs) + // return v.appL.StartApp(skyenv.VPNClientName, v.conf.Launcher.Apps[appindex].Args, envs) + } + return ErrProcNotAvailable +} + +// StopVPNClient implements API. +func (v *Visor) StopVPNClient(appName string) error { + // check process manager availability + if v.procM != nil { + _, err := v.appL.StopApp(appName) //nolint:errcheck + return err + } + return ErrProcNotAvailable +} + // SetAppDetailedStatus implements API. func (v *Visor) SetAppDetailedStatus(appName, status string) error { proc, ok := v.procM.ProcByName(appName) @@ -570,14 +618,14 @@ func (v *Visor) GetAppConnectionsSummary(appName string) ([]appserver.Connection if err != nil { return nil, err } - return cSummary, nil } return nil, ErrProcNotAvailable } // VPNServers gets available public VPN server from service discovery URL -func (v *Visor) VPNServers() ([]string, error) { +func (v *Visor) VPNServers() ([]servicedisc.Service, error) { + //func (v *Visor) VPNServers(version, country string) ([]servicedisc.Service, error) { //query filtering log := logging.MustGetLogger("vpnservers") vlog := logging.NewMasterLogger() vlog.SetLevel(logrus.InfoLevel) @@ -588,16 +636,13 @@ func (v *Visor) VPNServers() ([]string, error) { SK: v.conf.SK, DiscAddr: v.conf.Launcher.ServiceDisc, }, &http.Client{Timeout: time.Duration(1) * time.Second}, "") + // vpnServers, err := sdClient.Services(context.Background(), 0, version, country) //query filtering vpnServers, err := sdClient.Services(context.Background(), 0) if err != nil { v.log.Error("Error getting public vpn servers: ", err) return nil, err } - serverAddrs := make([]string, len(vpnServers)) - for idx, server := range vpnServers { - serverAddrs[idx] = server.Addr.PubKey().String() - } - return serverAddrs, nil + return vpnServers, nil } // RemoteVisors return list of connected remote visors diff --git a/pkg/visor/rpc.go b/pkg/visor/rpc.go index 1df30dfada..40dc85348e 100644 --- a/pkg/visor/rpc.go +++ b/pkg/visor/rpc.go @@ -12,6 +12,7 @@ import ( "github.com/skycoin/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire/pkg/app/appserver" "github.com/skycoin/skywire/pkg/routing" + "github.com/skycoin/skywire/pkg/servicedisc" "github.com/skycoin/skywire/pkg/transport" "github.com/skycoin/skywire/pkg/transport/network" "github.com/skycoin/skywire/pkg/util/rpcutil" @@ -76,7 +77,7 @@ func (r *RPC) Health(_ *struct{}, out *HealthInfo) (err error) { } /* - <<< NODE UPTIME >>> + <<< THIS NODE UPTIME >>> */ // Uptime returns for how long the visor has been running in seconds @@ -218,6 +219,20 @@ func (r *RPC) StopApp(name *string, _ *struct{}) (err error) { return r.visor.StopApp(*name) } +// StartVPNClient starts VPNClient App +func (r *RPC) StartVPNClient(pubkey *string, _ *struct{}) (err error) { + defer rpcutil.LogCall(r.log, "StartApp", pubkey)(nil, &err) + + return r.visor.StartVPNClient(*pubkey) +} + +// StopVPNClient stops VPNClient App +func (r *RPC) StopVPNClient(name *string, _ *struct{}) (err error) { + defer rpcutil.LogCall(r.log, "StopVPNClient", name)(nil, &err) + + return r.visor.StopVPNClient(*name) +} + // RestartApp restarts App with provided name. func (r *RPC) RestartApp(name *string, _ *struct{}) (err error) { defer rpcutil.LogCall(r.log, "RestartApp", name)(nil, &err) @@ -541,9 +556,19 @@ func (r *RPC) SetPublicAutoconnect(pAc *bool, _ *struct{}) (err error) { return err } +/* //query filtering +// FilterVPNServersIn is input for VPNServers +type FilterVPNServersIn struct { + Version string + Country string +} +*/ + // VPNServers gets available public VPN server from service discovery URL -func (r *RPC) VPNServers(_ *struct{}, out *[]string) (err error) { - defer rpcutil.LogCall(r.log, "RemoteVisor", nil)(out, &err) +func (r *RPC) VPNServers(_ *struct{}, out *[]servicedisc.Service) (err error) { + //func (r *RPC) VPNServers(vc *FilterVPNServersIn, _ *struct{}, out *[]servicedisc.Service) (err error) { //query filtering + defer rpcutil.LogCall(r.log, "VPNServers", nil)(out, &err) + // vpnServers, err := r.visor.VPNServers(vc.Version, vc.Country) //query filtering vpnServers, err := r.visor.VPNServers() if vpnServers != nil { *out = vpnServers diff --git a/pkg/visor/rpc_client.go b/pkg/visor/rpc_client.go index b7e7f319a0..d32fb774c2 100644 --- a/pkg/visor/rpc_client.go +++ b/pkg/visor/rpc_client.go @@ -22,6 +22,7 @@ import ( "github.com/skycoin/skywire/pkg/app/appserver" "github.com/skycoin/skywire/pkg/router" "github.com/skycoin/skywire/pkg/routing" + "github.com/skycoin/skywire/pkg/servicedisc" "github.com/skycoin/skywire/pkg/skyenv" "github.com/skycoin/skywire/pkg/transport" "github.com/skycoin/skywire/pkg/transport/network" @@ -143,6 +144,16 @@ func (rc *rpcClient) StopApp(appName string) error { return rc.Call("StopApp", &appName, &struct{}{}) } +// StartVPNClient calls StartVPNClient. +func (rc *rpcClient) StartVPNClient(pubkey string) error { + return rc.Call("StartVPNClient", &pubkey, &struct{}{}) +} + +// StopVPNClient calls StopVPNClient. +func (rc *rpcClient) StopVPNClient(appName string) error { + return rc.Call("StopVPNClient", &appName, &struct{}{}) +} + // SetAppDetailedStatus sets app's detailed state. func (rc *rpcClient) SetAppDetailedStatus(appName, status string) error { return rc.Call("SetAppDetailedStatus", &SetAppStatusIn{ @@ -398,8 +409,15 @@ type StatusMessage struct { } // VPNServers calls VPNServers. -func (rc *rpcClient) VPNServers() ([]string, error) { - output := []string{} +func (rc *rpcClient) VPNServers() ([]servicedisc.Service, error) { + //func (rc *rpcClient) VPNServers(version, country string) ([]servicedisc.Service, error) { //query filtering + output := []servicedisc.Service{} + /* //query filtering + rc.Call("VPNServers", &FilterVPNServersIn{ + Version: version, + Country: country, + }, &output) // nolint + */ rc.Call("VPNServers", &struct{}{}, &output) // nolint return output, nil } @@ -632,6 +650,16 @@ func (*mockRPCClient) StopApp(string) error { return nil } +// StartVPNClient implements API. +func (*mockRPCClient) StartVPNClient(string) error { + return nil +} + +// StopVPNClient implements API. +func (*mockRPCClient) StopVPNClient(string) error { + return nil +} + // SetAppDetailedStatus sets app's detailed state. func (mc *mockRPCClient) SetAppDetailedStatus(appName, status string) error { return mc.do(true, func() error { @@ -954,8 +982,9 @@ func (mc *mockRPCClient) GetPersistentTransports() ([]transport.PersistentTransp } // VPNServers implements API -func (mc *mockRPCClient) VPNServers() ([]string, error) { - return []string{}, nil +func (mc *mockRPCClient) VPNServers() ([]servicedisc.Service, error) { + //func (mc *mockRPCClient) VPNServers(_, _ string) ([]servicedisc.Service, error) { //query filtering + return []servicedisc.Service{}, nil } // RemoteVisors implements API