blob: f35b9f482682d7680d37059a77e360f56d7947b3 [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.
25func get(ctx context.Context) (bool, error) {
26 // 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 {
31 return false, fmt.Errorf("NewRequestWithContext: %w", err)
32 }
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 {
37 return false, fmt.Errorf("Do: %w", err)
38 }
39
40 defer res.Body.Close()
41
42 v := &vessel{}
43 err = json.NewDecoder(res.Body).Decode(v)
44 if err != nil {
45 return false, fmt.Errorf("Decode: %w", err)
46 }
47
Serge Bazanski5c1ab3c2021-03-27 15:57:16 +000048 if v.Speed == 0.0 {
Serge Bazanski3d116b22021-03-27 15:43:18 +000049 return true, nil
50 } else {
51 glog.Infof("Freed! %+v", v)
52 return false, nil
53 }
54}
55
56type shipState string
57
58const (
59 shipStateUnknown shipState = "UNKNOWN"
60 shipStateStuck shipState = "STUCK"
61 shipStateFreed shipState = "FREED"
62)
63
64type service struct {
65 lastStateMu sync.RWMutex
66 lastState shipState
67 lastStateTime time.Time
68}
69
70func (s *service) worker(ctx context.Context) {
71 update := func() {
72 state := shipStateUnknown
73 // shitty back off, good enough.
74 retries := 10
75 for {
76 stuck, err := get(ctx)
77 if err != nil {
78 glog.Warningf("get: %v", err)
79 if retries > 0 {
80 time.Sleep(60 * time.Second)
81 retries -= 1
82 } else {
83 glog.Errorf("giving up on get")
84 break
85 }
86 } else {
87 if stuck {
88 state = shipStateStuck
89 } else {
90 state = shipStateFreed
91 }
92 break
93 }
94 }
95
96 glog.Infof("New state: %v", state)
97 s.lastStateMu.Lock()
98 s.lastState = state
99 s.lastStateTime = time.Now()
100 s.lastStateMu.Unlock()
101 }
102
103 update()
104 ticker := time.NewTicker(15 * 60 * time.Second)
105 for {
106 select {
107 case <-ctx.Done():
108 return
109 case <-ticker.C:
110 update()
111 }
112 }
113}
114
115func timeMust(t time.Time, err error) time.Time {
116 if err != nil {
117 panic(err)
118 }
119 return t
120}
121
122var (
123 timeStuck = timeMust(time.Parse(
124 "At 15:04 Eastern European Time (MST) on 2 January 2006",
125 "At 07:40 Eastern European Time (UTC) on 23 March 2021",
126 ))
127)
128
129func (s *service) Status(ctx context.Context, req *pb.StatusRequest) (*pb.StatusResponse, error) {
130 s.lastStateMu.RLock()
131 state := s.lastState
132 lastChecked := s.lastStateTime
133 s.lastStateMu.RUnlock()
134
135 res := &pb.StatusResponse{
136 LastChecked: lastChecked.UnixNano(),
137 }
138 switch state {
139 case shipStateUnknown:
140 res.Current = pb.StatusResponse_STUCKNESS_UNKNOWN
141 case shipStateStuck:
142 res.Current = pb.StatusResponse_STUCKNESS_STUCK
143 res.Elapsed = time.Since(timeStuck).Nanoseconds()
144 case shipStateFreed:
145 res.Current = pb.StatusResponse_STUCKNESS_FREE
146 }
147
148 return res, nil
149}
150
151var (
152 flagPublicAddress string
153)
154
155func main() {
156 flag.StringVar(&flagPublicAddress, "public_address", "127.0.0.1:8080", "Public HTTP/JSON listen address")
157 flag.Parse()
158 m := mirko.New()
159 if err := m.Listen(); err != nil {
160 glog.Exitf("Listen(): %v", err)
161 }
162
163 s := &service{}
164 pb.RegisterShipStuckServer(m.GRPC(), s)
165
166 publicMux := runtime.NewServeMux()
167 publicSrv := http.Server{
168 Addr: flagPublicAddress,
169 Handler: publicMux,
170 }
171 go func() {
172 glog.Infof("REST listening on %s", flagPublicAddress)
173 if err := publicSrv.ListenAndServe(); err != nil {
174 glog.Exitf("public ListenAndServe: %v", err)
175 }
176 }()
177 if err := pb.RegisterShipStuckHandlerServer(m.Context(), publicMux, s); err != nil {
178 glog.Exitf("RegisterShipStuckHandlerSerever: %v", err)
179 }
180
181 go s.worker(m.Context())
182
183 if err := m.Serve(); err != nil {
184 glog.Exitf("Serve(): %v", err)
185 }
186
187 <-m.Done()
188}