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  }