| 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 { |
| // No idea what these fields are, but they seem to be related to |
| // latitude/longitude. Use these to detect the stuckness of the ship. |
| GT int64 `json:"gt"` |
| DW int64 `json:"dw"` |
| } |
| |
| // 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) |
| } |
| |
| if v.GT == 219079 && v.DW == 199489 { |
| 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( |
| "At 15:04 Eastern European Time (MST) on 2 January 2006", |
| "At 07:40 Eastern European Time (UTC) on 23 March 2021", |
| )) |
| ) |
| |
| 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() |
| } |