cwtch/app/cli/main.go

626 lines
20 KiB
Go
Raw Normal View History

2018-04-30 21:47:21 +00:00
package main
import (
2018-05-28 18:05:06 +00:00
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
peer2 "cwtch.im/cwtch/peer"
"bytes"
"cwtch.im/cwtch/model"
"fmt"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
2018-12-04 02:52:11 +00:00
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"github.com/c-bata/go-prompt"
"golang.org/x/crypto/ssh/terminal"
2018-06-29 19:04:52 +00:00
"os"
"os/user"
"path"
"strings"
"syscall"
"time"
2018-04-30 21:47:21 +00:00
)
var app app2.Application
var peer peer2.CwtchPeer
var group *model.Group
var groupFollowBreakChan chan bool
var prmpt string
var suggestionsBase = []prompt.Suggest{
{Text: "/new-profile", Description: "create a new profile"},
{Text: "/load-profiles", Description: "loads profiles with a password"},
{Text: "/list-profiles", Description: "list active profiles"},
{Text: "/select-profile", Description: "selects an active profile to use"},
{Text: "/help", Description: "print list of commands"},
{Text: "/quit", Description: "quit cwtch"},
2018-05-03 05:46:42 +00:00
}
var suggestionsSelectedProfile = []prompt.Suggest{
{Text: "/info", Description: "show user info"},
{Text: "/list-contacts", Description: "retrieve a list of contacts"},
{Text: "/list-groups", Description: "retrieve a list of groups"},
{Text: "/new-group", Description: "create a new group on a server"},
{Text: "/select-group", Description: "selects a group to follow"},
{Text: "/unselect-group", Description: "stop following the current group"},
{Text: "/invite", Description: "invite a new contact"},
{Text: "/invite-to-group", Description: "invite an existing contact to join an existing group"},
{Text: "/accept-invite", Description: "accept the invite of a group"},
/*{Text: "/list-servers", Description: "retrieve a list of servers and their connection status"},
{Text: "/list-peers", Description: "retrieve a list of peers and their connection status"},*/
{Text: "/export-group", Description: "export a group invite: prints as a string"},
{Text: "/trust", Description: "trust a peer"},
{Text: "/block", Description: "block a peer - you will no longer see messages or connect to this peer"},
}
var suggestions = suggestionsBase
2018-05-03 06:01:15 +00:00
var usages = map[string]string{
"/new-profile": "/new-profile [name]",
"/load-profiles": "/load-profiles",
"/list-profiles": "",
"/select-profile": "/select-profile [onion]",
"/quit": "",
/* "/list-servers": "",
"/list-peers": "",*/
"/list-contacts": "",
"/list-groups": "",
"/select-group": "/select-group [groupid]",
"/unselect-group": "",
"/export-group": "/export-group [groupid]",
"/info": "",
"/send": "/send [groupid] [message]",
"/timeline": "/timeline [groupid]",
"/accept-invite": "/accept-invite [groupid]",
"/invite": "/invite [peerid]",
"/invite-to-group": "/invite-to-group [groupid] [peerid]",
"/new-group": "/new-group [server]",
"/help": "",
"/trust": "/trust [peerid]",
"/block": "/block [peerid]",
}
func printMessage(m model.Message) {
p := peer.GetContact(m.PeerID)
name := "unknown"
if p != nil {
name = p.Name
} else if peer.GetOnion() == m.PeerID {
name = peer.GetName()
}
2018-10-05 03:18:34 +00:00
fmt.Printf("%v %v (%v): %v\n", m.Timestamp, name, m.PeerID, m.Message)
}
func startGroupFollow() {
groupFollowBreakChan = make(chan bool)
go func() {
for {
l := len(group.Timeline.GetMessages())
select {
case <-time.After(1 * time.Second):
if group == nil {
return
}
gms := group.Timeline.GetMessages()
if len(gms) != l {
fmt.Printf("\n")
for ; l < len(gms); l++ {
printMessage(gms[l])
}
fmt.Printf(prmpt)
}
case <-groupFollowBreakChan:
return
}
}
}()
}
func stopGroupFollow() {
if group != nil {
groupFollowBreakChan <- true
group = nil
}
2018-05-03 05:46:42 +00:00
}
2018-05-03 04:12:45 +00:00
func completer(d prompt.Document) []prompt.Suggest {
2018-05-20 18:29:46 +00:00
var s []prompt.Suggest
2018-05-03 04:12:45 +00:00
if d.FindStartOfPreviousWord() == 0 {
2018-05-03 05:46:42 +00:00
return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true)
2018-05-03 04:12:45 +00:00
}
w := d.CurrentLine()
// Suggest a profile id
if strings.HasPrefix(w, "/select-profile") {
s = []prompt.Suggest{}
peerlist := app.ListPeers()
for onion, peername := range peerlist {
s = append(s, prompt.Suggest{Text: onion, Description: peername})
}
}
if peer == nil {
2018-08-04 21:19:11 +00:00
return s
}
// Suggest groupid
if /*strings.HasPrefix(w, "send") || strings.HasPrefix(w, "timeline") ||*/ strings.HasPrefix(w, "/export-group") || strings.HasPrefix(w, "/select-group") {
2018-05-03 04:12:45 +00:00
s = []prompt.Suggest{}
groups := peer.GetGroups()
for _, groupID := range groups {
group := peer.GetGroup(groupID)
2018-05-03 04:12:45 +00:00
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
// Suggest unaccepted group
if strings.HasPrefix(w, "/accept-invite") {
s = []prompt.Suggest{}
groups := peer.GetGroups()
for _, groupID := range groups {
group := peer.GetGroup(groupID)
if group.Accepted == false {
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
}
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
// suggest groupid AND peerid
if strings.HasPrefix(w, "/invite-to-group") {
2018-05-03 04:12:45 +00:00
if d.FindStartOfPreviousWordWithSpace() == 0 {
s = []prompt.Suggest{}
groups := peer.GetGroups()
for _, groupID := range groups {
group := peer.GetGroup(groupID)
if group.Owner == "self" {
s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer})
}
2018-05-03 04:12:45 +00:00
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
2018-05-16 21:11:04 +00:00
}
2018-05-16 21:11:04 +00:00
s = []prompt.Suggest{}
contacts := peer.GetContacts()
for _, onion := range contacts {
contact := peer.GetContact(onion)
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
2018-05-03 04:12:45 +00:00
}
2018-05-16 21:11:04 +00:00
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
2018-05-03 04:12:45 +00:00
}
// Suggest contact onion / peerid
if strings.HasPrefix(w, "/block") || strings.HasPrefix(w, "/trust") || strings.HasPrefix(w, "/invite") {
2018-05-03 04:12:45 +00:00
s = []prompt.Suggest{}
contacts := peer.GetContacts()
for _, onion := range contacts {
contact := peer.GetContact(onion)
s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name})
2018-05-03 04:12:45 +00:00
}
return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
}
return s
}
func handleAppEvents(em event.Manager) {
queue := event.NewQueue()
em.Subscribe(event.NewPeer, queue)
em.Subscribe(event.PeerError, queue)
for {
ev := queue.Next()
switch ev.EventType {
case event.NewPeer:
onion := ev.Data[event.Identity]
p := app.GetPeer(onion)
app.LaunchPeers()
fmt.Printf("\nLoaded profile %v (%v)\n", p.GetName(), p.GetOnion())
suggestions = append(suggestionsBase, suggestionsSelectedProfile...)
profiles := app.ListPeers()
fmt.Printf("\n%v profiles active now\n", len(profiles))
fmt.Printf("You should run `select-profile` to use a profile or `list-profiles` to view loaded profiles\n")
case event.PeerError:
err := ev.Data[event.Error]
fmt.Printf("\nError creating profile: %v\n", err)
}
}
}
2018-04-30 21:47:21 +00:00
func main() {
2018-05-03 04:12:45 +00:00
cwtch :=
2018-05-03 06:01:15 +00:00
`
#, #'
@@@@@@:
@@@@@@.
@'@@+#' @@@@+
''''''@ #+@ :
@''''+;+' . '
@''@' :+' , ; ##, +'
,@@ ;' #'#@''. #''@''#
# ''''''#:,,#'''''@
: @''''@ :+'''@
' @;+'@ @'#
.:# '#..# '# @
@@@@@@
@@@@@@
'@@@@
@# . .
+++, #'@+'@
''', ''''''#
.#+# ''', @'''+,
@''# ''', .#@
:; '@''# .;. ''', ' : ;. ,
@+'''@ '+'+ @++ @+'@+''''+@ #+'''#: ''';#''+@ @@@@ @@@@@@@@@ :@@@@#
2018-05-03 04:12:45 +00:00
#''''''# +''. +'': +'''''''''+ @'''''''# '''+'''''@ @@@@ @@@@@@@@@@@@@@@@:
@'''@@'''@ @''# ,'''@ ''+ @@''+#+ :'''@@+''' ''''@@'''' @@@@ @@@@@@@@@@@@@@@@@
'''# @''# +''@ @'''# ;''@ +''+ @''@ ,+'', '''@ #'''. @@@@ @@@@ '@@@# @@@@
;''' @@; '''# #'@'' @''@ @''+ +''# .@@ ''', '''. @@@@ @@@ @@@ .@@@
@''# #'' ''#''#@''. #''# '''. '''. +'', @@@@ @@@ @@@ @@@
@''# @''@'' #'@+'+ #''# '''. ''', +'', +@@@.@@@ @@@@ @@@, @@@ ,@@@
;''+ @, +''@'# @'+''@ @''# +''; '+ ''', +'', @@@@@@@@# @@@@ @@@. .@@@ .@@@
'''# ++'+ ''''@ ,''''# #''' @''@ '@''+ ''', ''', @@@@@@@@: @@@@ @@@; .@@@' ;@@@
@'''@@'''@ #'''. +'''' ;'''#@ :'''#@+''+ ''', ''', @@@@@@# @@@@ @@@+ ,@@@. @@@@
#''''''# @''+ @''+ +'''' @'''''''# ''', ''', #@@@. @@@@ @@@+ @@@ @@@@
@+''+@ '++@ ;++@ '#''@ ##'''@: +++, +++, :@ @@@@ @@@' @@@ '@@@
:' ' '`
fmt.Printf("%v\n\n", cwtch)
2018-04-30 21:47:21 +00:00
quit := false
usr, err := user.Current()
if err != nil {
2018-12-04 02:52:11 +00:00
log.Errorf("\nError: could not load current user: %v\n", err)
os.Exit(1)
}
2018-11-22 18:01:04 +00:00
acn, err := connectivity.StartTor(path.Join(usr.HomeDir, ".cwtch"), "")
if err != nil {
2018-12-04 02:52:11 +00:00
log.Errorf("\nError connecting to Tor: %v\n", err)
os.Exit(1)
}
2018-11-22 18:01:04 +00:00
app = app2.NewApp(acn, path.Join(usr.HomeDir, ".cwtch"))
go handleAppEvents(app.GetPrimaryBus())
if err != nil {
2018-12-04 02:52:11 +00:00
log.Errorf("Error initializing application: %v", err)
os.Exit(1)
}
2019-01-04 21:44:21 +00:00
log.SetLevel(log.LevelDebug)
fmt.Printf("\nWelcome to Cwtch!\n")
fmt.Printf("If this if your first time you should create a profile by running `/new-profile`\n")
fmt.Printf("`/load-profiles` will prompt you for a password and load profiles from storage\n")
fmt.Printf("`/help` will show you other available commands\n")
fmt.Printf("There is full [TAB] completion support\n\n")
2018-05-03 04:12:45 +00:00
var history []string
2018-04-30 21:47:21 +00:00
for !quit {
prmpt = "cwtch> "
if group != nil {
prmpt = fmt.Sprintf("cwtch %v (%v) [%v] say> ", peer.GetName(), peer.GetOnion(), group.GroupID)
} else if peer != nil {
prmpt = fmt.Sprintf("cwtch %v (%v)> ", peer.GetName(), peer.GetOnion())
2018-04-30 21:47:21 +00:00
}
2018-05-03 04:12:45 +00:00
text := prompt.Input(prmpt, completer, prompt.OptionSuggestionBGColor(prompt.Purple),
2018-05-03 06:01:15 +00:00
prompt.OptionDescriptionBGColor(prompt.White),
2019-01-04 21:44:21 +00:00
prompt.OptionPrefixTextColor(prompt.White),
prompt.OptionInputTextColor(prompt.Purple),
2018-05-03 06:01:15 +00:00
prompt.OptionHistory(history))
2018-05-03 04:12:45 +00:00
2018-05-03 06:01:15 +00:00
commands := strings.Split(text[0:], " ")
history = append(history, text)
2018-08-04 21:19:11 +00:00
if peer == nil {
if commands[0] != "/help" && commands[0] != "/quit" && commands[0] != "/new-profile" && commands[0] != "/load-profiles" && commands[0] != "/select-profile" && commands[0] != "/list-profiles" {
2018-08-04 21:19:11 +00:00
fmt.Printf("Profile needs to be set\n")
continue
}
}
// Send
if group != nil && !strings.HasPrefix(commands[0], "/") {
err := peer.SendMessageToGroup(group.GroupID, text)
if err != nil {
fmt.Printf("Error sending message: %v\n", err)
}
}
2018-04-30 21:47:21 +00:00
switch commands[0] {
case "/quit":
2018-04-30 21:47:21 +00:00
quit = true
case "/new-profile":
if len(commands) == 2 {
name := strings.Trim(commands[1], " ")
if name == "" {
fmt.Printf("Error creating profile, usage: %v\n", usages[commands[0]])
break
}
fmt.Print("** WARNING: PASSWORDS CANNOT BE RECOVERED! **\n")
password := ""
failcount := 0
for ; failcount < 3; failcount++ {
fmt.Print("Enter a password to encrypt the profile: ")
bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin))
if string(bytePassword) == "" {
fmt.Print("\nBlank password not allowed.")
continue
}
fmt.Print("\nRe-enter password: ")
bytePassword2, _ := terminal.ReadPassword(int(syscall.Stdin))
if bytes.Equal(bytePassword, bytePassword2) {
password = string(bytePassword)
break
} else {
fmt.Print("\nPASSWORDS DIDN'T MATCH! Try again.\n")
}
2018-04-30 21:47:21 +00:00
}
if failcount >= 3 {
fmt.Printf("Error creating profile for %v: Your password entries must match!\n", name)
} else {
app.CreatePeer(name, password)
}
2018-04-30 21:47:21 +00:00
} else {
fmt.Printf("Error creating New Profile, usage: %s\n", usages[commands[0]])
}
case "/load-profiles":
fmt.Print("Enter a password to decrypt the profile: ")
bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
2019-01-28 20:09:25 +00:00
if err != nil {
fmt.Printf("\nError loading profiles: %v\n", err)
continue
}
app.LoadProfiles(string(bytePassword))
if err == nil {
} else {
fmt.Printf("\nError loading profiles: %v\n", err)
2018-04-30 21:47:21 +00:00
}
2018-10-05 03:18:34 +00:00
case "/list-profiles":
peerlist := app.ListPeers()
for onion, peername := range peerlist {
fmt.Printf(" %v\t%v\n", onion, peername)
}
case "/select-profile":
2018-04-30 21:47:21 +00:00
if len(commands) == 2 {
p := app.GetPeer(commands[1])
if p == nil {
fmt.Printf("Error: profile '%v' does not exist\n", commands[1])
2018-04-30 21:47:21 +00:00
} else {
stopGroupFollow()
peer = p
suggestions = append(suggestionsBase, suggestionsSelectedProfile...)
2018-04-30 21:47:21 +00:00
}
2018-10-05 03:18:34 +00:00
2018-10-06 03:50:55 +00:00
// Auto cwtchPeer / Join Server
2018-10-05 03:18:34 +00:00
// TODO There are some privacy implications with this that we should
// think over.
for _, name := range p.GetContacts() {
2018-10-05 03:18:34 +00:00
profile := p.GetContact(name)
if profile.Trusted && !profile.Blocked {
p.PeerWithOnion(profile.Onion)
}
}
for _, groupid := range p.GetGroups() {
group := p.GetGroup(groupid)
if group.Accepted || group.Owner == "self" {
p.JoinServer(group.GroupServer)
}
}
2018-04-30 21:47:21 +00:00
} else {
fmt.Printf("Error selecting profile, usage: %s\n", usages[commands[0]])
2018-04-30 21:47:21 +00:00
}
case "/info":
if peer != nil {
fmt.Printf("Address cwtch:%v\n", peer.GetOnion())
2018-04-30 21:47:21 +00:00
} else {
2018-05-03 04:12:45 +00:00
fmt.Printf("Profile needs to be set\n")
2018-04-30 21:47:21 +00:00
}
case "/invite":
if len(commands) == 2 {
fmt.Printf("Inviting cwtch:%v\n", commands[1])
peer.PeerWithOnion(commands[1])
} else {
fmt.Printf("Error inviting peer, usage: %s\n", usages[commands[0]])
}
/*case "/list-peers":
peers := peer.GetPeers()
for p, s := range peers {
fmt.Printf("Name: %v Status: %v\n", p, connections.ConnectionStateName[s])
}
case "/list-servers":
servers := peer.GetServers()
2018-05-03 04:12:45 +00:00
for s, st := range servers {
fmt.Printf("Name: %v Status: %v\n", s, connections.ConnectionStateName[st])
}*/
case "/list-contacts":
contacts := peer.GetContacts()
for _, onion := range contacts {
c := peer.GetContact(onion)
fmt.Printf("Name: %v Onion: %v Trusted: %v\n", c.Name, c.Onion, c.Trusted)
}
case "/list-groups":
for _, gid := range peer.GetGroups() {
g := peer.GetGroup(gid)
fmt.Printf("Group Id: %v Owner: %v Accepted:%v\n", gid, g.Owner, g.Accepted)
2018-05-03 04:12:45 +00:00
}
case "/trust":
if len(commands) == 2 {
peer.TrustPeer(commands[1])
} else {
fmt.Printf("Error trusting peer, usage: %s\n", usages[commands[0]])
}
case "/block":
if len(commands) == 2 {
peer.BlockPeer(commands[1])
} else {
fmt.Printf("Error blocking peer, usage: %s\n", usages[commands[0]])
}
case "/accept-invite":
2018-05-03 04:12:45 +00:00
if len(commands) == 2 {
2018-05-03 06:01:15 +00:00
groupID := commands[1]
err := peer.AcceptInvite(groupID)
if err != nil {
2018-05-03 04:12:45 +00:00
fmt.Printf("Error: %v\n", err)
} else {
group := peer.GetGroup(groupID)
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
peer.JoinServer(group.GroupServer)
}
2018-05-03 04:12:45 +00:00
}
} else {
fmt.Printf("Error accepting invite, usage: %s\n", usages[commands[0]])
2018-05-01 21:36:03 +00:00
}
case "/invite-to-group":
2018-05-01 21:36:03 +00:00
if len(commands) == 3 {
fmt.Printf("Inviting %v to %v\n", commands[1], commands[2])
err := peer.InviteOnionToGroup(commands[2], commands[1])
2018-05-03 04:12:45 +00:00
if err != nil {
fmt.Printf("Error: %v\n", err)
2018-05-01 21:36:03 +00:00
}
} else {
fmt.Printf("Error inviting peer to group, usage: %s\n", usages[commands[0]])
2018-05-01 21:36:03 +00:00
}
case "/new-group":
if len(commands) == 2 && commands[1] != "" {
2018-05-01 21:36:03 +00:00
fmt.Printf("Setting up a new group on server:%v\n", commands[1])
id, _, err := peer.StartGroup(commands[1])
2018-05-16 20:18:47 +00:00
if err == nil {
fmt.Printf("New Group [%v] created for server %v\n", id, commands[1])
group := peer.GetGroup(id)
2018-05-16 20:18:47 +00:00
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
peer.JoinServer(group.GroupServer)
2018-05-16 20:18:47 +00:00
}
} else {
2018-05-16 20:18:47 +00:00
fmt.Printf("Error creating new group: %v", err)
}
2018-05-01 21:36:03 +00:00
} else {
fmt.Printf("Error creating a new group, usage: %s\n", usages[commands[0]])
2018-05-01 21:36:03 +00:00
}
case "/select-group":
2018-05-03 04:12:45 +00:00
if len(commands) == 2 {
g := peer.GetGroup(commands[1])
if g == nil {
fmt.Printf("Error: group %s not found!\n", commands[1])
2018-05-03 04:12:45 +00:00
} else {
stopGroupFollow()
group = g
fmt.Printf("--------------- %v ---------------\n", group.GroupID)
gms := group.Timeline.GetMessages()
max := 20
if len(gms) < max {
max = len(gms)
}
for i := len(gms) - max; i < len(gms); i++ {
printMessage(gms[i])
2018-05-03 04:12:45 +00:00
}
fmt.Printf("------------------------------\n")
startGroupFollow()
2018-05-03 04:12:45 +00:00
}
} else {
fmt.Printf("Error selecting a group, usage: %s\n", usages[commands[0]])
2018-05-03 04:12:45 +00:00
}
case "/unselect-group":
stopGroupFollow()
case "/export-group":
if len(commands) == 2 {
group := peer.GetGroup(commands[1])
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
invite, _ := peer.ExportGroup(commands[1])
fmt.Printf("Invite: %v\n", invite)
}
} else {
fmt.Printf("Error exporting group, usage: %s\n", usages[commands[0]])
}
2018-10-15 00:59:53 +00:00
case "/import-group":
if len(commands) == 2 {
err := peer.ImportGroup(commands[1])
2018-10-15 00:59:53 +00:00
if err != nil {
fmt.Printf("Error importing group: %v\n", err)
} else {
fmt.Printf("Imported group!\n")
2018-10-15 00:59:53 +00:00
}
} else {
fmt.Printf("%v", commands)
fmt.Printf("Error importing group, usage: %s\n", usages[commands[0]])
}
case "/help":
2018-05-03 05:46:42 +00:00
for _, command := range suggestions {
fmt.Printf("%-18s%-56s%s\n", command.Text, command.Description, usages[command.Text])
}
case "/sendlots":
if len(commands) == 2 {
group := peer.GetGroup(commands[1])
if group == nil {
fmt.Printf("Error: group does not exist\n")
} else {
for i := 0; i < 100; i++ {
fmt.Printf("Sending message: %v\n", i)
err := peer.SendMessageToGroup(commands[1], fmt.Sprintf("this is message %v", i))
if err != nil {
fmt.Printf("could not send message %v because %v\n", i, err)
}
}
fmt.Printf("Waiting 5 seconds for message to process...\n")
time.Sleep(time.Second * 5)
timeline := group.GetTimeline()
totalLatency := time.Duration(0)
maxLatency := time.Duration(0)
totalMessages := 0
for i := 0; i < 100; i++ {
found := false
2018-05-20 18:29:46 +00:00
for _, m := range timeline {
if m.Message == fmt.Sprintf("this is message %v", i) && m.PeerID == peer.GetOnion() {
found = true
latency := m.Received.Sub(m.Timestamp)
fmt.Printf("Latency for Message %v was %v\n", i, latency)
totalLatency = totalLatency + latency
if maxLatency < latency {
maxLatency = latency
}
2018-05-16 21:11:04 +00:00
totalMessages++
}
}
if !found {
fmt.Printf("message %v was never received\n", i)
}
}
fmt.Printf("Average Latency for %v messages was: %vms\n", totalMessages, time.Duration(int64(totalLatency)/int64(totalMessages)))
fmt.Printf("Max Latency for %v messages was: %vms\n", totalMessages, maxLatency)
}
}
2018-04-30 21:47:21 +00:00
}
2018-10-06 03:50:55 +00:00
}
app.Shutdown()
2018-11-22 18:01:04 +00:00
acn.Close()
2018-06-29 19:04:52 +00:00
os.Exit(0)
}