blob: 0224c3dc77ef97480dd296ff70e1abbe3d7c1375 [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 {
20 // No idea what these fields are, but they seem to be related to
21 // latitude/longitude. Use these to detect the stuckness of the ship.
22 GT int64 `json:"gt"`
23 DW int64 `json:"dw"`
24}
25
26// get retrieves the current status of the ship - returns true if stack, false
27// otherwise.
28func get(ctx context.Context) (bool, error) {
29 // Sorry vesselfinder, if you made it easy to set up an account I would
30 // gladly pay for the API instead of doing this. Limiting requests to once
31 // every 15 minutes though, that seems fair enough.
32 req, err := http.NewRequestWithContext(ctx, "GET", "https://www.vesselfinder.com/api/pub/click/353136000", nil)
33 if err != nil {
34 return false, fmt.Errorf("NewRequestWithContext: %w", err)
35 }
36 req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0")
37
38 res, err := http.DefaultClient.Do(req)
39 if err != nil {
40 return false, fmt.Errorf("Do: %w", err)
41 }
42
43 defer res.Body.Close()
44
45 v := &vessel{}
46 err = json.NewDecoder(res.Body).Decode(v)
47 if err != nil {
48 return false, fmt.Errorf("Decode: %w", err)
49 }
50
51 if v.GT == 219079 && v.DW == 199489 {
52 return true, nil
53 } else {
54 glog.Infof("Freed! %+v", v)
55 return false, nil
56 }
57}
58
59type shipState string
60
61const (
62 shipStateUnknown shipState = "UNKNOWN"
63 shipStateStuck shipState = "STUCK"
64 shipStateFreed shipState = "FREED"
65)
66
67type service struct {
68 lastStateMu sync.RWMutex
69 lastState shipState
70 lastStateTime time.Time
71}
72
73func (s *service) worker(ctx context.Context) {
74 update := func() {
75 state := shipStateUnknown
76 // shitty back off, good enough.
77 retries := 10
78 for {
79 stuck, err := get(ctx)
80 if err != nil {
81 glog.Warningf("get: %v", err)
82 if retries > 0 {
83 time.Sleep(60 * time.Second)
84 retries -= 1
85 } else {
86 glog.Errorf("giving up on get")
87 break
88 }
89 } else {
90 if stuck {
91 state = shipStateStuck
92 } else {
93 state = shipStateFreed
94 }
95 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(
127 "At 15:04 Eastern European Time (MST) on 2 January 2006",
128 "At 07:40 Eastern European Time (UTC) on 23 March 2021",
129 ))
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
149 }
150
151 return res, nil
152}
153
154var (
155 flagPublicAddress string
156)
157
158func main() {
159 flag.StringVar(&flagPublicAddress, "public_address", "127.0.0.1:8080", "Public HTTP/JSON listen address")
160 flag.Parse()
161 m := mirko.New()
162 if err := m.Listen(); err != nil {
163 glog.Exitf("Listen(): %v", err)
164 }
165
166 s := &service{}
167 pb.RegisterShipStuckServer(m.GRPC(), s)
168
169 publicMux := runtime.NewServeMux()
170 publicSrv := http.Server{
171 Addr: flagPublicAddress,
172 Handler: publicMux,
173 }
174 go func() {
175 glog.Infof("REST listening on %s", flagPublicAddress)
176 if err := publicSrv.ListenAndServe(); err != nil {
177 glog.Exitf("public ListenAndServe: %v", err)
178 }
179 }()
180 if err := pb.RegisterShipStuckHandlerServer(m.Context(), publicMux, s); err != nil {
181 glog.Exitf("RegisterShipStuckHandlerSerever: %v", err)
182 }
183
184 go s.worker(m.Context())
185
186 if err := m.Serve(); err != nil {
187 glog.Exitf("Serve(): %v", err)
188 }
189
190 <-m.Done()
191}