github.com/safing/portbase@v0.19.5/api/client/client.go (about) 1 package client 2 3 import ( 4 "fmt" 5 "sync" 6 "time" 7 8 "github.com/tevino/abool" 9 10 "github.com/safing/portbase/log" 11 ) 12 13 const ( 14 backOffTimer = 1 * time.Second 15 16 offlineSignal uint8 = 0 17 onlineSignal uint8 = 1 18 ) 19 20 // The Client enables easy interaction with the API. 21 type Client struct { 22 sync.Mutex 23 24 server string 25 26 onlineSignal chan struct{} 27 offlineSignal chan struct{} 28 shutdownSignal chan struct{} 29 lastSignal uint8 30 31 send chan *Message 32 resend chan *Message 33 recv chan *Message 34 35 operations map[string]*Operation 36 nextOpID uint64 37 38 lastError string 39 } 40 41 // NewClient returns a new Client. 42 func NewClient(server string) *Client { 43 c := &Client{ 44 server: server, 45 onlineSignal: make(chan struct{}), 46 offlineSignal: make(chan struct{}), 47 shutdownSignal: make(chan struct{}), 48 lastSignal: offlineSignal, 49 send: make(chan *Message, 100), 50 resend: make(chan *Message, 1), 51 recv: make(chan *Message, 100), 52 operations: make(map[string]*Operation), 53 } 54 go c.handler() 55 return c 56 } 57 58 // Connect connects to the API once. 59 func (c *Client) Connect() error { 60 defer c.signalOffline() 61 62 err := c.wsConnect() 63 if err != nil && err.Error() != c.lastError { 64 log.Errorf("client: error connecting to Portmaster: %s", err) 65 c.lastError = err.Error() 66 } 67 return err 68 } 69 70 // StayConnected calls Connect again whenever the connection is lost. 71 func (c *Client) StayConnected() { 72 log.Infof("client: connecting to Portmaster at %s", c.server) 73 74 _ = c.Connect() 75 for { 76 select { 77 case <-time.After(backOffTimer): 78 log.Infof("client: reconnecting...") 79 _ = c.Connect() 80 case <-c.shutdownSignal: 81 return 82 } 83 } 84 } 85 86 // Shutdown shuts the client down. 87 func (c *Client) Shutdown() { 88 select { 89 case <-c.shutdownSignal: 90 default: 91 close(c.shutdownSignal) 92 } 93 } 94 95 func (c *Client) signalOnline() { 96 c.Lock() 97 defer c.Unlock() 98 if c.lastSignal == offlineSignal { 99 log.Infof("client: went online") 100 c.offlineSignal = make(chan struct{}) 101 close(c.onlineSignal) 102 c.lastSignal = onlineSignal 103 104 // resend unsent request 105 for _, op := range c.operations { 106 if op.resuscitationEnabled.IsSet() && op.request.sent != nil && op.request.sent.SetToIf(true, false) { 107 op.client.send <- op.request 108 log.Infof("client: resuscitated %s %s %s", op.request.OpID, op.request.Type, op.request.Key) 109 } 110 } 111 112 } 113 } 114 115 func (c *Client) signalOffline() { 116 c.Lock() 117 defer c.Unlock() 118 if c.lastSignal == onlineSignal { 119 log.Infof("client: went offline") 120 c.onlineSignal = make(chan struct{}) 121 close(c.offlineSignal) 122 c.lastSignal = offlineSignal 123 124 // signal offline status to operations 125 for _, op := range c.operations { 126 op.handle(&Message{ 127 OpID: op.ID, 128 Type: MsgOffline, 129 }) 130 } 131 132 } 133 } 134 135 // Online returns a closed channel read if the client is connected to the API. 136 func (c *Client) Online() <-chan struct{} { 137 c.Lock() 138 defer c.Unlock() 139 return c.onlineSignal 140 } 141 142 // Offline returns a closed channel read if the client is not connected to the API. 143 func (c *Client) Offline() <-chan struct{} { 144 c.Lock() 145 defer c.Unlock() 146 return c.offlineSignal 147 } 148 149 func (c *Client) handler() { 150 for { 151 select { 152 153 case m := <-c.recv: 154 155 if m == nil { 156 return 157 } 158 159 c.Lock() 160 op, ok := c.operations[m.OpID] 161 c.Unlock() 162 163 if ok { 164 log.Tracef("client: [%s] received %s msg: %s", m.OpID, m.Type, m.Key) 165 op.handle(m) 166 } else { 167 log.Tracef("client: received message for unknown operation %s", m.OpID) 168 } 169 170 case <-c.shutdownSignal: 171 return 172 173 } 174 } 175 } 176 177 // Operation represents a single operation by a client. 178 type Operation struct { 179 ID string 180 request *Message 181 client *Client 182 handleFunc func(*Message) 183 handler chan *Message 184 resuscitationEnabled *abool.AtomicBool 185 } 186 187 func (op *Operation) handle(m *Message) { 188 if op.handleFunc != nil { 189 op.handleFunc(m) 190 } else { 191 select { 192 case op.handler <- m: 193 default: 194 log.Warningf("client: handler channel of operation %s overflowed", op.ID) 195 } 196 } 197 } 198 199 // Cancel the operation. 200 func (op *Operation) Cancel() { 201 op.client.Lock() 202 defer op.client.Unlock() 203 delete(op.client.operations, op.ID) 204 close(op.handler) 205 } 206 207 // Send sends a request to the API. 208 func (op *Operation) Send(command, text string, data interface{}) { 209 op.request = &Message{ 210 OpID: op.ID, 211 Type: command, 212 Key: text, 213 Value: data, 214 sent: abool.NewBool(false), 215 } 216 log.Tracef("client: [%s] sending %s msg: %s", op.request.OpID, op.request.Type, op.request.Key) 217 op.client.send <- op.request 218 } 219 220 // EnableResuscitation will resend the request after reconnecting to the API. 221 func (op *Operation) EnableResuscitation() { 222 op.resuscitationEnabled.Set() 223 } 224 225 // NewOperation returns a new operation. 226 func (c *Client) NewOperation(handleFunc func(*Message)) *Operation { 227 c.Lock() 228 defer c.Unlock() 229 230 c.nextOpID++ 231 op := &Operation{ 232 ID: fmt.Sprintf("#%d", c.nextOpID), 233 client: c, 234 handleFunc: handleFunc, 235 handler: make(chan *Message, 100), 236 resuscitationEnabled: abool.NewBool(false), 237 } 238 c.operations[op.ID] = op 239 return op 240 }