diff --git a/cli/cli.go b/cli/cli.go index f485184..377567a 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "fmt" rpc "github.com/special/notricochet/rpc" "google.golang.org/grpc" @@ -13,21 +14,23 @@ const ( defaultAddress = "127.0.0.1:58281" ) -func main() { - conn, err := grpc.Dial(defaultAddress, grpc.WithInsecure()) - if err != nil { - fmt.Printf("connection failed: %v\n", err) - os.Exit(1) - } - defer conn.Close() +var LogBuffer bytes.Buffer +func main() { input, err := readline.NewEx(&readline.Config{}) if err != nil { fmt.Println(err) os.Exit(1) } defer input.Close() - log.SetOutput(input.Stdout()) + log.SetOutput(&LogBuffer) + + conn, err := grpc.Dial(defaultAddress, grpc.WithInsecure()) + if err != nil { + fmt.Printf("connection failed: %v\n", err) + os.Exit(1) + } + defer conn.Close() client := &Client{ Backend: rpc.NewRicochetCoreClient(conn), @@ -38,11 +41,17 @@ func main() { } client.Ui = ui - if err := client.Initialize(); err != nil { - fmt.Println(err) - os.Exit(1) - } + fmt.Print("Connecting to backend...\n") + + go func() { + if err := client.Initialize(); err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + client.Block() + ui.PrintStatus() + client.Unblock() + }() - go client.Run() ui.CommandLoop() } diff --git a/cli/client.go b/cli/client.go index fb9483f..80dc6a9 100644 --- a/cli/client.go +++ b/cli/client.go @@ -17,10 +17,14 @@ type Client struct { NetworkStatus ricochet.NetworkStatus Contacts *ContactList - monitorsChannel chan interface{} - blockChannel chan struct{} - unblockChannel chan struct{} - populatedContacts bool + monitorsChannel chan interface{} + blockChannel chan struct{} + unblockChannel chan struct{} + + populatedChannel chan struct{} + populatedNetwork bool + populatedContacts bool + populatedConversations bool } // XXX need to handle backend connection loss/reconnection.. @@ -29,6 +33,7 @@ func (c *Client) Initialize() error { c.monitorsChannel = make(chan interface{}, 10) c.blockChannel = make(chan struct{}) c.unblockChannel = make(chan struct{}) + c.populatedChannel = make(chan struct{}) // Query server status and version status, err := c.Backend.GetServerStatus(context.Background(), &ricochet.ServerStatusRequest{ @@ -39,7 +44,7 @@ func (c *Client) Initialize() error { } c.ServerStatus = *status if status.RpcVersion != 1 { - return fmt.Errorf("Unsupported backend RPC version %d", status.RpcVersion) + return fmt.Errorf("unsupported backend RPC version %d", status.RpcVersion) } // Query identity @@ -54,7 +59,11 @@ func (c *Client) Initialize() error { go c.monitorContacts() // Conversation monitor isn't started until contacts are populated - // XXX block until populated/initialized? + // Spawn routine to handle all events + go c.Run() + + // Block until all state is populated + <-c.populatedChannel return nil } @@ -86,6 +95,18 @@ func (c *Client) Unblock() { c.unblockChannel <- struct{}{} } +func (c *Client) IsInitialized() bool { + return c.populatedChannel == nil +} + +func (c *Client) checkIfPopulated() { + if c.populatedChannel != nil && c.populatedContacts && + c.populatedConversations && c.populatedNetwork { + close(c.populatedChannel) + c.populatedChannel = nil + } +} + func (c *Client) monitorNetwork() { stream, err := c.Backend.MonitorNetwork(context.Background(), &ricochet.MonitorNetworkRequest{}) if err != nil { @@ -149,6 +170,8 @@ func (c *Client) monitorConversations() { func (c *Client) onNetworkStatus(status *ricochet.NetworkStatus) { log.Printf("Network status changed: %v", status) c.NetworkStatus = *status + c.populatedNetwork = true + c.checkIfPopulated() } func (c *Client) onContactEvent(event *ricochet.ContactEvent) { @@ -161,10 +184,13 @@ func (c *Client) onContactEvent(event *ricochet.ContactEvent) { switch event.Type { case ricochet.ContactEvent_POPULATE: - // Populate is terminated by a nil subject - if event.Subject == nil { + if c.populatedContacts { + log.Printf("Ignoring unexpected contact populate event: %v", event) + } else if event.Subject == nil { + // Populate is terminated by a nil subject c.populatedContacts = true log.Printf("Loaded %d contacts", len(c.Contacts.Contacts)) + c.checkIfPopulated() go c.monitorConversations() } else if data != nil { c.Contacts.Populate(data) @@ -218,6 +244,13 @@ func (c *Client) onConversationEvent(event *ricochet.ConversationEvent) { } message := event.Msg + + if event.Type == ricochet.ConversationEvent_POPULATE && message == nil { + c.populatedConversations = true + c.checkIfPopulated() + return + } + if message == nil || message.Recipient == nil || message.Sender == nil { log.Printf("Ignoring invalid conversation event: %v", event) return @@ -236,8 +269,11 @@ func (c *Client) onConversationEvent(event *ricochet.ConversationEvent) { return } - c.Ui.PrintMessage(remoteContact, message.Sender.IsSelf, message.Text) + if event.Type != ricochet.ConversationEvent_POPULATE { + c.Ui.PrintMessage(remoteContact, message.Sender.IsSelf, message.Text) + } + // XXX Shouldn't mark until displayed if !message.Sender.IsSelf { backend := c.Backend message := message @@ -252,3 +288,19 @@ func (c *Client) onConversationEvent(event *ricochet.ConversationEvent) { }() } } + +func (c *Client) NetworkControlStatus() ricochet.TorControlStatus { + if c.NetworkStatus.Control != nil { + return *c.NetworkStatus.Control + } else { + return ricochet.TorControlStatus{} + } +} + +func (c *Client) NetworkConnectionStatus() ricochet.TorConnectionStatus { + if c.NetworkStatus.Connection != nil { + return *c.NetworkStatus.Connection + } else { + return ricochet.TorConnectionStatus{} + } +} diff --git a/cli/ui.go b/cli/ui.go index 4bc92f9..7f50a96 100644 --- a/cli/ui.go +++ b/cli/ui.go @@ -87,16 +87,54 @@ func (ui *UI) Execute(line string) error { case "contacts": ui.ListContacts() + case "log": + fmt.Print(LogBuffer.String()) + case "help": fallthrough default: - fmt.Println("Commands: clear, quit, status, connect, disconnect, contacts, help") + fmt.Println("Commands: clear, quit, status, connect, disconnect, contacts, log, help") } return nil } +func (ui *UI) PrintStatus() { + controlStatus := ui.Client.NetworkControlStatus() + connectionStatus := ui.Client.NetworkConnectionStatus() + + switch controlStatus.Status { + case ricochet.TorControlStatus_STOPPED: + fmt.Fprintf(ui.Input.Stdout(), "Network is stopped -- type 'connect' to go online\n") + + case ricochet.TorControlStatus_ERROR: + fmt.Fprintf(ui.Input.Stdout(), "Network error: %s\n", controlStatus.ErrorMessage) + + case ricochet.TorControlStatus_CONNECTING: + fmt.Fprintf(ui.Input.Stdout(), "Network connecting...\n") + + case ricochet.TorControlStatus_CONNECTED: + switch connectionStatus.Status { + case ricochet.TorConnectionStatus_UNKNOWN: + fallthrough + case ricochet.TorConnectionStatus_OFFLINE: + fmt.Fprintf(ui.Input.Stdout(), "Network is offline\n") + + case ricochet.TorConnectionStatus_BOOTSTRAPPING: + fmt.Fprintf(ui.Input.Stdout(), "Network bootstrapping: %s\n", connectionStatus.BootstrapProgress) + + case ricochet.TorConnectionStatus_READY: + fmt.Fprintf(ui.Input.Stdout(), "Network is online\n") + } + } + + fmt.Fprintf(ui.Input.Stdout(), "Your ricochet ID is %s\n", ui.Client.Identity.Address) + + // no. contacts, contact reqs, online contacts + // unread messages +} + func (ui *UI) PrintMessage(contact *Contact, outbound bool, text string) { if contact == ui.CurrentContact { if outbound {