From c8bfe4663eed5349200aaf321089e6f2f05e508b Mon Sep 17 00:00:00 2001 From: John Brooks Date: Sat, 15 Oct 2016 22:57:16 -0600 Subject: [PATCH] cli: Separate UI logic and fix client threadsafety --- cli/cli.go | 109 +++------------------------------ cli/client.go | 50 ++++++---------- cli/ui.go | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 132 deletions(-) create mode 100644 cli/ui.go diff --git a/cli/cli.go b/cli/cli.go index 3fb0940..f485184 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -3,13 +3,10 @@ package main import ( "fmt" rpc "github.com/special/notricochet/rpc" - "golang.org/x/net/context" "google.golang.org/grpc" "gopkg.in/readline.v1" "log" "os" - "strconv" - "strings" ) const ( @@ -32,108 +29,20 @@ func main() { defer input.Close() log.SetOutput(input.Stdout()) - c := &Client{ + client := &Client{ Backend: rpc.NewRicochetCoreClient(conn), - Input: input, } + ui := &UI{ + Input: input, + Client: client, + } + client.Ui = ui - if err := c.Initialize(); err != nil { + if err := client.Initialize(); err != nil { fmt.Println(err) os.Exit(1) } - go c.Run() - - input.SetPrompt("> ") - for { - line := input.Line() - if line.CanContinue() { - continue - } else if line.CanBreak() { - break - } - - words := strings.SplitN(line.Line, " ", 1) - - if id, err := strconv.Atoi(words[0]); err == nil { - contact := c.Contacts.ById(int32(id)) - if contact != nil { - c.SetCurrentContact(contact) - } else { - fmt.Printf("no contact %d\n", id) - } - continue - } - - if c.CurrentContact != nil { - if len(words[0]) > 0 && words[0][0] == '/' { - words[0] = words[0][1:] - } else { - _, err := c.Backend.SendMessage(context.Background(), &rpc.Message{ - Sender: &rpc.Entity{IsSelf: true}, - Recipient: &rpc.Entity{ - ContactId: c.CurrentContact.Data.Id, - Address: c.CurrentContact.Data.Address, - }, - Text: line.Line, - }) - if err != nil { - fmt.Printf("send message error: %v\n", err) - } - continue - } - } - - switch words[0] { - case "clear": - readline.ClearScreen(readline.Stdout) - - case "quit": - os.Exit(0) - - case "status": - fmt.Printf("server: %v\n", c.ServerStatus) - fmt.Printf("identity: %v\n", c.Identity) - - case "connect": - status, err := c.Backend.StartNetwork(context.Background(), &rpc.StartNetworkRequest{}) - if err != nil { - fmt.Printf("start network error: %v\n", err) - } else { - fmt.Printf("network started: %v\n", status) - } - - case "disconnect": - status, err := c.Backend.StopNetwork(context.Background(), &rpc.StopNetworkRequest{}) - if err != nil { - fmt.Printf("stop network error: %v\n", err) - } else { - fmt.Printf("network stopped: %v\n", status) - } - - case "contacts": - byStatus := make(map[rpc.Contact_Status][]*Contact) - for _, contact := range c.Contacts.Contacts { - byStatus[contact.Data.Status] = append(byStatus[contact.Data.Status], contact) - } - - order := []rpc.Contact_Status{rpc.Contact_ONLINE, rpc.Contact_UNKNOWN, rpc.Contact_OFFLINE, rpc.Contact_REQUEST, rpc.Contact_REJECTED} - for _, status := range order { - contacts := byStatus[status] - if len(contacts) == 0 { - continue - } - fmt.Printf(". %s\n", strings.ToLower(status.String())) - for _, contact := range contacts { - fmt.Printf("... [%d] %s\n", contact.Data.Id, contact.Data.Nickname) - } - } - - case "help": - fallthrough - - default: - fmt.Println("Commands: clear, quit, status, connect, disconnect, contacts, help") - } - } + go client.Run() + ui.CommandLoop() } diff --git a/cli/client.go b/cli/client.go index 7dab2b0..fb9483f 100644 --- a/cli/client.go +++ b/cli/client.go @@ -4,23 +4,22 @@ import ( "fmt" "github.com/special/notricochet/rpc" "golang.org/x/net/context" - "gopkg.in/readline.v1" "log" - "strings" ) type Client struct { Backend ricochet.RicochetCoreClient - Input *readline.Instance + Ui *UI ServerStatus ricochet.ServerStatusReply Identity ricochet.Identity - NetworkStatus ricochet.NetworkStatus - Contacts *ContactList - CurrentContact *Contact + NetworkStatus ricochet.NetworkStatus + Contacts *ContactList monitorsChannel chan interface{} + blockChannel chan struct{} + unblockChannel chan struct{} populatedContacts bool } @@ -28,6 +27,8 @@ type Client struct { func (c *Client) Initialize() error { c.Contacts = NewContactList() c.monitorsChannel = make(chan interface{}, 10) + c.blockChannel = make(chan struct{}) + c.unblockChannel = make(chan struct{}) // Query server status and version status, err := c.Backend.GetServerStatus(context.Background(), &ricochet.ServerStatusRequest{ @@ -71,24 +72,18 @@ func (c *Client) Run() { default: log.Panicf("Unknown event type on monitor channel: %v", event) } + case <-c.blockChannel: + <-c.unblockChannel } } } -func (c *Client) SetCurrentContact(contact *Contact) { - c.CurrentContact = contact - if c.CurrentContact != nil { - config := *c.Input.Config - config.Prompt = fmt.Sprintf("%s > ", c.CurrentContact.Data.Nickname) - config.UniqueEditLine = true - c.Input.SetConfig(&config) - fmt.Printf("--- %s (%s) ---\n", c.CurrentContact.Data.Nickname, strings.ToLower(c.CurrentContact.Data.Status.String())) - } else { - config := *c.Input.Config - config.Prompt = "> " - config.UniqueEditLine = false - c.Input.SetConfig(&config) - } +func (c *Client) Block() { + c.blockChannel <- struct{}{} +} + +func (c *Client) Unblock() { + c.unblockChannel <- struct{}{} } func (c *Client) monitorNetwork() { @@ -206,8 +201,8 @@ func (c *Client) onContactEvent(event *ricochet.ContactEvent) { contact, _ := c.Contacts.Deleted(data) - if c.CurrentContact == contact { - c.SetCurrentContact(nil) + if c.Ui.CurrentContact == contact { + c.Ui.SetCurrentContact(nil) } default: @@ -241,16 +236,7 @@ func (c *Client) onConversationEvent(event *ricochet.ConversationEvent) { return } - if remoteContact == c.CurrentContact { - // XXX so unsafe - if message.Sender.IsSelf { - fmt.Fprintf(c.Input.Stdout(), "\r%s > %s\n", remoteContact.Data.Nickname, message.Text) - } else { - fmt.Fprintf(c.Input.Stdout(), "\r%s < %s\n", remoteContact.Data.Nickname, message.Text) - } - } else if !message.Sender.IsSelf { - fmt.Fprintf(c.Input.Stdout(), "\r---- %s < %s\n", remoteContact.Data.Nickname, message.Text) - } + c.Ui.PrintMessage(remoteContact, message.Sender.IsSelf, message.Text) if !message.Sender.IsSelf { backend := c.Backend diff --git a/cli/ui.go b/cli/ui.go new file mode 100644 index 0000000..4bc92f9 --- /dev/null +++ b/cli/ui.go @@ -0,0 +1,163 @@ +package main + +import ( + "errors" + "fmt" + "github.com/special/notricochet/rpc" + "golang.org/x/net/context" + "gopkg.in/readline.v1" + "strconv" + "strings" +) + +type UI struct { + Input *readline.Instance + Client *Client + + CurrentContact *Contact +} + +func (ui *UI) CommandLoop() { + ui.Input.SetPrompt("> ") + for { + line, err := ui.Input.Readline() + if err != nil { + return + } + + if err := ui.Execute(line); err != nil { + return + } + } +} + +func (ui *UI) Execute(line string) error { + // Block client event handlers for threadsafety + ui.Client.Block() + defer ui.Client.Unblock() + + words := strings.SplitN(line, " ", 1) + + if ui.CurrentContact != nil { + if len(words[0]) > 0 && words[0][0] == '/' { + words[0] = words[0][1:] + } else { + ui.SendMessage(line) + return nil + } + } + + if id, err := strconv.Atoi(words[0]); err == nil { + contact := ui.Client.Contacts.ById(int32(id)) + if contact != nil { + ui.SetCurrentContact(contact) + } else { + fmt.Printf("no contact %d\n", id) + } + return nil + } + + switch words[0] { + case "clear": + readline.ClearScreen(readline.Stdout) + + case "quit": + return errors.New("Quitting") + + case "status": + fmt.Printf("server: %v\n", ui.Client.ServerStatus) + fmt.Printf("identity: %v\n", ui.Client.Identity) + + case "connect": + status, err := ui.Client.Backend.StartNetwork(context.Background(), &ricochet.StartNetworkRequest{}) + if err != nil { + fmt.Printf("start network error: %v\n", err) + } else { + fmt.Printf("network started: %v\n", status) + } + + case "disconnect": + status, err := ui.Client.Backend.StopNetwork(context.Background(), &ricochet.StopNetworkRequest{}) + if err != nil { + fmt.Printf("stop network error: %v\n", err) + } else { + fmt.Printf("network stopped: %v\n", status) + } + + case "contacts": + ui.ListContacts() + + case "help": + fallthrough + + default: + fmt.Println("Commands: clear, quit, status, connect, disconnect, contacts, help") + } + + return nil +} + +func (ui *UI) PrintMessage(contact *Contact, outbound bool, text string) { + if contact == ui.CurrentContact { + if outbound { + fmt.Fprintf(ui.Input.Stdout(), "\r%s > %s\n", contact.Data.Nickname, text) + } else { + fmt.Fprintf(ui.Input.Stdout(), "\r%s < %s\n", contact.Data.Nickname, text) + } + } else if !outbound { + fmt.Fprintf(ui.Input.Stdout(), "\r---- %s < %s\n", contact.Data.Nickname, text) + } +} + +func (ui *UI) SendMessage(text string) { + _, err := ui.Client.Backend.SendMessage(context.Background(), &ricochet.Message{ + Sender: &ricochet.Entity{IsSelf: true}, + Recipient: &ricochet.Entity{ + ContactId: ui.CurrentContact.Data.Id, + Address: ui.CurrentContact.Data.Address, + }, + Text: text, + }) + if err != nil { + fmt.Printf("send message error: %v\n", err) + } +} + +func (ui *UI) ListContacts() { + byStatus := make(map[ricochet.Contact_Status][]*Contact) + for _, contact := range ui.Client.Contacts.Contacts { + byStatus[contact.Data.Status] = append(byStatus[contact.Data.Status], contact) + } + + order := []ricochet.Contact_Status{ricochet.Contact_ONLINE, ricochet.Contact_UNKNOWN, ricochet.Contact_OFFLINE, ricochet.Contact_REQUEST, ricochet.Contact_REJECTED} + for _, status := range order { + contacts := byStatus[status] + if len(contacts) == 0 { + continue + } + fmt.Printf(". %s\n", strings.ToLower(status.String())) + for _, contact := range contacts { + fmt.Printf("... [%d] %s\n", contact.Data.Id, contact.Data.Nickname) + } + } +} + +func (ui *UI) SetCurrentContact(contact *Contact) { + if ui.CurrentContact == contact { + return + } + + ui.CurrentContact = contact + if ui.CurrentContact != nil { + config := *ui.Input.Config + config.Prompt = fmt.Sprintf("%s > ", contact.Data.Nickname) + config.UniqueEditLine = true + ui.Input.SetConfig(&config) + fmt.Printf("--- %s (%s) ---\n", contact.Data.Nickname, strings.ToLower(contact.Data.Status.String())) + } else { + config := *ui.Input.Config + config.Prompt = "> " + config.UniqueEditLine = false + ui.Input.SetConfig(&config) + } +}