2021-03-27 15:43:18 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"code.hackerspace.pl/hscloud/go/mirko"
|
|
|
|
"github.com/golang/glog"
|
|
|
|
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
|
|
|
|
|
|
|
pb "code.hackerspace.pl/hscloud/personal/q3k/shipstuck/proto"
|
|
|
|
)
|
|
|
|
|
|
|
|
type vessel struct {
|
2021-03-27 15:57:16 +00:00
|
|
|
Speed float64 `json:"ss"`
|
2021-03-27 15:43:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// get retrieves the current status of the ship - returns true if stack, false
|
|
|
|
// otherwise.
|
|
|
|
func get(ctx context.Context) (bool, error) {
|
|
|
|
// Sorry vesselfinder, if you made it easy to set up an account I would
|
|
|
|
// gladly pay for the API instead of doing this. Limiting requests to once
|
|
|
|
// every 15 minutes though, that seems fair enough.
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.vesselfinder.com/api/pub/click/353136000", nil)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("NewRequestWithContext: %w", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0")
|
|
|
|
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Do: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
v := &vessel{}
|
|
|
|
err = json.NewDecoder(res.Body).Decode(v)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Decode: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-03-28 20:04:58 +00:00
|
|
|
if v.Speed < 0.5 {
|
2021-03-27 15:43:18 +00:00
|
|
|
return true, nil
|
|
|
|
} else {
|
|
|
|
glog.Infof("Freed! %+v", v)
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type shipState string
|
|
|
|
|
|
|
|
const (
|
|
|
|
shipStateUnknown shipState = "UNKNOWN"
|
|
|
|
shipStateStuck shipState = "STUCK"
|
|
|
|
shipStateFreed shipState = "FREED"
|
|
|
|
)
|
|
|
|
|
|
|
|
type service struct {
|
|
|
|
lastStateMu sync.RWMutex
|
|
|
|
lastState shipState
|
|
|
|
lastStateTime time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *service) worker(ctx context.Context) {
|
|
|
|
update := func() {
|
|
|
|
state := shipStateUnknown
|
|
|
|
// shitty back off, good enough.
|
|
|
|
retries := 10
|
|
|
|
for {
|
|
|
|
stuck, err := get(ctx)
|
|
|
|
if err != nil {
|
|
|
|
glog.Warningf("get: %v", err)
|
|
|
|
if retries > 0 {
|
|
|
|
time.Sleep(60 * time.Second)
|
|
|
|
retries -= 1
|
|
|
|
} else {
|
|
|
|
glog.Errorf("giving up on get")
|
|
|
|
break
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if stuck {
|
|
|
|
state = shipStateStuck
|
|
|
|
} else {
|
|
|
|
state = shipStateFreed
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
glog.Infof("New state: %v", state)
|
|
|
|
s.lastStateMu.Lock()
|
|
|
|
s.lastState = state
|
|
|
|
s.lastStateTime = time.Now()
|
|
|
|
s.lastStateMu.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
update()
|
|
|
|
ticker := time.NewTicker(15 * 60 * time.Second)
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
case <-ticker.C:
|
|
|
|
update()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func timeMust(t time.Time, err error) time.Time {
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
timeStuck = timeMust(time.Parse(
|
2021-03-27 16:03:45 +00:00
|
|
|
"At 15:04 Eastern European Time (-0700) on 2 January 2006",
|
|
|
|
"At 07:40 Eastern European Time (+0200) on 23 March 2021",
|
2021-03-27 15:43:18 +00:00
|
|
|
))
|
|
|
|
)
|
|
|
|
|
|
|
|
func (s *service) Status(ctx context.Context, req *pb.StatusRequest) (*pb.StatusResponse, error) {
|
|
|
|
s.lastStateMu.RLock()
|
|
|
|
state := s.lastState
|
|
|
|
lastChecked := s.lastStateTime
|
|
|
|
s.lastStateMu.RUnlock()
|
|
|
|
|
|
|
|
res := &pb.StatusResponse{
|
|
|
|
LastChecked: lastChecked.UnixNano(),
|
|
|
|
}
|
|
|
|
switch state {
|
|
|
|
case shipStateUnknown:
|
|
|
|
res.Current = pb.StatusResponse_STUCKNESS_UNKNOWN
|
|
|
|
case shipStateStuck:
|
|
|
|
res.Current = pb.StatusResponse_STUCKNESS_STUCK
|
|
|
|
res.Elapsed = time.Since(timeStuck).Nanoseconds()
|
|
|
|
case shipStateFreed:
|
|
|
|
res.Current = pb.StatusResponse_STUCKNESS_FREE
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
flagPublicAddress string
|
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
flag.StringVar(&flagPublicAddress, "public_address", "127.0.0.1:8080", "Public HTTP/JSON listen address")
|
|
|
|
flag.Parse()
|
|
|
|
m := mirko.New()
|
|
|
|
if err := m.Listen(); err != nil {
|
|
|
|
glog.Exitf("Listen(): %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
s := &service{}
|
|
|
|
pb.RegisterShipStuckServer(m.GRPC(), s)
|
|
|
|
|
|
|
|
publicMux := runtime.NewServeMux()
|
|
|
|
publicSrv := http.Server{
|
|
|
|
Addr: flagPublicAddress,
|
|
|
|
Handler: publicMux,
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
glog.Infof("REST listening on %s", flagPublicAddress)
|
|
|
|
if err := publicSrv.ListenAndServe(); err != nil {
|
|
|
|
glog.Exitf("public ListenAndServe: %v", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
if err := pb.RegisterShipStuckHandlerServer(m.Context(), publicMux, s); err != nil {
|
|
|
|
glog.Exitf("RegisterShipStuckHandlerSerever: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
go s.worker(m.Context())
|
|
|
|
|
|
|
|
if err := m.Serve(); err != nil {
|
|
|
|
glog.Exitf("Serve(): %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
<-m.Done()
|
|
|
|
}
|