blob: a43c5e719bb5772e4b5465b4899707e16dd5608a [file] [log] [blame]
Serge Bazanski3d116b22021-03-27 15:43:18 +00001package main
2
3import (
4 "context"
5 "encoding/json"
6 "flag"
7 "fmt"
8 "net/http"
9 "sync"
10 "time"
11
12 "code.hackerspace.pl/hscloud/go/mirko"
13 "github.com/golang/glog"
14 "github.com/grpc-ecosystem/grpc-gateway/runtime"
15
16 pb "code.hackerspace.pl/hscloud/personal/q3k/shipstuck/proto"
17)
18
19type vessel struct {
Serge Bazanski5c1ab3c2021-03-27 15:57:16 +000020 Speed float64 `json:"ss"`
Serge Bazanski3d116b22021-03-27 15:43:18 +000021}
22
23// get retrieves the current status of the ship - returns true if stack, false
24// otherwise.
Serge Bazanskia4ae66b2021-03-29 13:26:37 +000025func get(ctx context.Context) (shipState, error) {
Serge Bazanski3d116b22021-03-27 15:43:18 +000026 // Sorry vesselfinder, if you made it easy to set up an account I would
27 // gladly pay for the API instead of doing this. Limiting requests to once
28 // every 15 minutes though, that seems fair enough.
29 req, err := http.NewRequestWithContext(ctx, "GET", "https://www.vesselfinder.com/api/pub/click/353136000", nil)
30 if err != nil {
Serge Bazanskia4ae66b2021-03-29 13:26:37 +000031 return shipStateUnknown, fmt.Errorf("NewRequestWithContext: %w", err)
Serge Bazanski3d116b22021-03-27 15:43:18 +000032 }
33 req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0")
34
35 res, err := http.DefaultClient.Do(req)
36 if err != nil {
Serge Bazanskia4ae66b2021-03-29 13:26:37 +000037 // vesselfinder.com down on 2021/03/29, add a 'cached' status for one hour as a hack.
38 if time.Now().Unix() < (1617024611 + 3600) {
39 return shipStateTowed, nil
40 glog.Warningf("Faking vesselfinder return.")
41 }
42 return shipStateUnknown, fmt.Errorf("Do: %w", err)
Serge Bazanski3d116b22021-03-27 15:43:18 +000043 }
44
45 defer res.Body.Close()
46
47 v := &vessel{}
48 err = json.NewDecoder(res.Body).Decode(v)
49 if err != nil {
Serge Bazanskia4ae66b2021-03-29 13:26:37 +000050 return shipStateUnknown, fmt.Errorf("Decode: %w", err)
Serge Bazanski3d116b22021-03-27 15:43:18 +000051 }
52
Serge Bazanskia4ae66b2021-03-29 13:26:37 +000053 if v.Speed > 10.0 {
54 return shipStateFreed, nil
Serge Bazanski3d116b22021-03-27 15:43:18 +000055 }
Serge Bazanskia4ae66b2021-03-29 13:26:37 +000056 if v.Speed > 0.5 {
57 return shipStateTowed, nil
58 }
59 return shipStateStuck, nil
Serge Bazanski3d116b22021-03-27 15:43:18 +000060}
61
62type shipState string
63
64const (
65 shipStateUnknown shipState = "UNKNOWN"
66 shipStateStuck shipState = "STUCK"
67 shipStateFreed shipState = "FREED"
Serge Bazanskia4ae66b2021-03-29 13:26:37 +000068 shipStateTowed shipState = "TOWED"
Serge Bazanski3d116b22021-03-27 15:43:18 +000069)
70
71type service struct {
72 lastStateMu sync.RWMutex
73 lastState shipState
74 lastStateTime time.Time
75}
76
77func (s *service) worker(ctx context.Context) {
78 update := func() {
79 state := shipStateUnknown
80 // shitty back off, good enough.
81 retries := 10
82 for {
83 stuck, err := get(ctx)
84 if err != nil {
85 glog.Warningf("get: %v", err)
86 if retries > 0 {
87 time.Sleep(60 * time.Second)
88 retries -= 1
89 } else {
90 glog.Errorf("giving up on get")
91 break
92 }
93 } else {
Serge Bazanskia4ae66b2021-03-29 13:26:37 +000094 state = stuck
Serge Bazanski3d116b22021-03-27 15:43:18 +000095 break
96 }
97 }
98
99 glog.Infof("New state: %v", state)
100 s.lastStateMu.Lock()
101 s.lastState = state
102 s.lastStateTime = time.Now()
103 s.lastStateMu.Unlock()
104 }
105
106 update()
107 ticker := time.NewTicker(15 * 60 * time.Second)
108 for {
109 select {
110 case <-ctx.Done():
111 return
112 case <-ticker.C:
113 update()
114 }
115 }
116}
117
118func timeMust(t time.Time, err error) time.Time {
119 if err != nil {
120 panic(err)
121 }
122 return t
123}
124
125var (
126 timeStuck = timeMust(time.Parse(
Serge Bazanski5da04942021-03-27 16:03:45 +0000127 "At 15:04 Eastern European Time (-0700) on 2 January 2006",
128 "At 07:40 Eastern European Time (+0200) on 23 March 2021",
Serge Bazanski3d116b22021-03-27 15:43:18 +0000129 ))
130)
131
132func (s *service) Status(ctx context.Context, req *pb.StatusRequest) (*pb.StatusResponse, error) {
133 s.lastStateMu.RLock()
134 state := s.lastState
135 lastChecked := s.lastStateTime
136 s.lastStateMu.RUnlock()
137
138 res := &pb.StatusResponse{
139 LastChecked: lastChecked.UnixNano(),
140 }
141 switch state {
142 case shipStateUnknown:
143 res.Current = pb.StatusResponse_STUCKNESS_UNKNOWN
144 case shipStateStuck:
145 res.Current = pb.StatusResponse_STUCKNESS_STUCK
146 res.Elapsed = time.Since(timeStuck).Nanoseconds()
147 case shipStateFreed:
148 res.Current = pb.StatusResponse_STUCKNESS_FREE
Serge Bazanskia4ae66b2021-03-29 13:26:37 +0000149 case shipStateTowed:
150 res.Current = pb.StatusResponse_STUCKNESS_TOWED
151 res.Elapsed = time.Since(timeStuck).Nanoseconds()
Serge Bazanski3d116b22021-03-27 15:43:18 +0000152 }
153
154 return res, nil
155}
156
157var (
158 flagPublicAddress string
159)
160
161func main() {
162 flag.StringVar(&flagPublicAddress, "public_address", "127.0.0.1:8080", "Public HTTP/JSON listen address")
163 flag.Parse()
164 m := mirko.New()
165 if err := m.Listen(); err != nil {
166 glog.Exitf("Listen(): %v", err)
167 }
168
169 s := &service{}
170 pb.RegisterShipStuckServer(m.GRPC(), s)
171
172 publicMux := runtime.NewServeMux()
173 publicSrv := http.Server{
174 Addr: flagPublicAddress,
175 Handler: publicMux,
176 }
177 go func() {
178 glog.Infof("REST listening on %s", flagPublicAddress)
179 if err := publicSrv.ListenAndServe(); err != nil {
180 glog.Exitf("public ListenAndServe: %v", err)
181 }
182 }()
183 if err := pb.RegisterShipStuckHandlerServer(m.Context(), publicMux, s); err != nil {
184 glog.Exitf("RegisterShipStuckHandlerSerever: %v", err)
185 }
186
187 go s.worker(m.Context())
188
189 if err := m.Serve(); err != nil {
190 glog.Exitf("Serve(): %v", err)
191 }
192
193 <-m.Done()
194}