]> Sergey Matveev's repositories - btrtrc.git/blob - cmd/btrtrc/fifos.go
cmd/btrtrc client
[btrtrc.git] / cmd / btrtrc / fifos.go
1 package main
2
3 import (
4         "bufio"
5         "bytes"
6         "encoding/hex"
7         "fmt"
8         "log"
9         "os"
10         "path"
11         "sort"
12         "strconv"
13         "strings"
14         "sync"
15         "syscall"
16         "time"
17
18         "github.com/anacrolix/dht/v2"
19         "github.com/anacrolix/torrent"
20         "github.com/anacrolix/torrent/metainfo"
21         "github.com/anacrolix/torrent/storage"
22         "github.com/anacrolix/torrent/types/infohash"
23         "github.com/dustin/go-humanize"
24 )
25
26 const (
27         MaxListNameWidth = 40
28         PeersDir         = "peers"
29         FilesDir         = "files"
30 )
31
32 type TorrentStat struct {
33         stats   torrent.ConnStats
34         rxSpeed int64
35         txSpeed int64
36 }
37
38 var (
39         FIFOsDir      = "fifos"
40         TorrentStats  = map[metainfo.Hash]TorrentStat{}
41         TorrentStatsM sync.RWMutex
42         Torrents      []metainfo.Hash
43         TorrentsM     sync.RWMutex
44 )
45
46 func recreateFIFO(pth string) {
47         os.Remove(pth)
48         if err := syscall.Mkfifo(pth, 0666); err != nil {
49                 log.Fatalln(err)
50         }
51 }
52
53 func shortenName(name string) string {
54         s := []rune(name)
55         if len(s) > MaxListNameWidth {
56                 s = s[:MaxListNameWidth]
57         }
58         return string(s)
59 }
60
61 func fifoList(c *torrent.Client) {
62         pth := path.Join(FIFOsDir, "list")
63         recreateFIFO(pth)
64         for {
65                 fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
66                 if err != nil {
67                         log.Println("OpenFile:", pth, err)
68                         time.Sleep(time.Second)
69                         continue
70                 }
71                 ts := make([]*torrent.Torrent, 0, len(Torrents))
72                 TorrentsM.RLock()
73                 for _, h := range Torrents {
74                         t, _ := c.Torrent(h)
75                         if t == nil || t.Info() == nil {
76                                 fmt.Fprintf(fd, "%s not ready\n", t.Name())
77                                 continue
78                         }
79                         ts = append(ts, t)
80                 }
81                 TorrentsM.RUnlock()
82                 for _, t := range ts {
83                         stats := t.Stats()
84                         done := t.BytesCompleted() * 100 / t.Length()
85                         percColour := Red
86                         if done == 100 {
87                                 percColour = Green
88                         }
89                         tx := stats.BytesWrittenData.Int64()
90                         tx += TxStats[t.InfoHash()]
91                         ratio := float64(tx) / float64(t.Length())
92                         TorrentStatsM.RLock()
93                         prev := TorrentStats[t.InfoHash()]
94                         TorrentStatsM.RUnlock()
95                         var eta string
96                         if done < 100 && prev.rxSpeed > 0 {
97                                 etaRaw := time.Duration((t.Length() - t.BytesCompleted()) / prev.rxSpeed)
98                                 etaRaw *= time.Second
99                                 eta = etaRaw.String()
100                         }
101                         fmt.Fprintf(fd,
102                                 "%s%s%s %s%40s%s %8s %s%3d%%%s %4.1f %s%d%s/%s%d%s %d/%d/%d/%d %s\n",
103                                 Blue, t.InfoHash().HexString(), Reset,
104                                 Green, shortenName(t.Name()), Reset,
105                                 humanize.IBytes(uint64(t.Length())),
106                                 percColour, done, Reset,
107                                 ratio,
108                                 Green, prev.rxSpeed/1024, Reset,
109                                 Magenta, prev.txSpeed/1024, Reset,
110                                 stats.TotalPeers,
111                                 stats.PendingPeers,
112                                 stats.ActivePeers,
113                                 stats.ConnectedSeeders,
114                                 eta,
115                         )
116                 }
117                 fd.Close()
118                 time.Sleep(time.Second)
119         }
120 }
121
122 func mustParseInt(s string) int {
123         i, err := strconv.Atoi(s)
124         if err != nil {
125                 log.Fatalln(err)
126         }
127         return i
128 }
129
130 func fifoPeerList(t *torrent.Torrent) {
131         pth := path.Join(FIFOsDir, PeersDir, t.InfoHash().HexString())
132         recreateFIFO(pth)
133         for {
134                 fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
135                 if err != nil {
136                         if os.IsNotExist(err) {
137                                 break
138                         }
139                         log.Println("OpenFile:", pth, err)
140                         time.Sleep(time.Second)
141                         continue
142                 }
143                 pcs := t.PeerConns()
144                 sort.Sort(ByPeerID(pcs))
145                 for _, pc := range pcs {
146                         cols := strings.Split(pc.CompletedString(), "/")
147                         done := (mustParseInt(cols[0]) * 100) / mustParseInt(cols[1])
148                         doneColour := Red
149                         if done == 100 {
150                                 doneColour = Green
151                         }
152                         stats := pc.Peer.Stats()
153                         fmt.Fprintf(fd,
154                                 "%s%s%s %10s %s%3d%%%s %s%d%s/%s%d%s | %s%s%s / %s%s%s | %s%s%s %q\n",
155                                 Blue, hex.EncodeToString(pc.PeerID[:]), Reset,
156                                 pc.StatusFlags(),
157                                 doneColour, done, Reset,
158                                 Green, int(pc.DownloadRate()/1024), Reset,
159                                 Magenta, int(pc.UploadRate()/1024), Reset,
160                                 Green, humanize.IBytes(uint64(stats.BytesReadData.Int64())), Reset,
161                                 Magenta, humanize.IBytes(uint64(stats.BytesWrittenData.Int64())), Reset,
162                                 Green, pc.RemoteAddr, Reset,
163                                 pc.PeerClientName,
164                         )
165                 }
166                 fd.Close()
167                 time.Sleep(time.Second)
168         }
169 }
170
171 func fifoFileList(t *torrent.Torrent) {
172         pth := path.Join(FIFOsDir, FilesDir, t.InfoHash().HexString())
173         recreateFIFO(pth)
174         for {
175                 fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
176                 if err != nil {
177                         if os.IsNotExist(err) {
178                                 break
179                         }
180                         log.Println("OpenFile:", pth, err)
181                         time.Sleep(time.Second)
182                         continue
183                 }
184                 for n, f := range t.Files() {
185                         var done int64
186                         if f.Length() > 0 {
187                                 done = (f.BytesCompleted() * 100) / f.Length()
188                         }
189                         percColour := Green
190                         if done < 100 {
191                                 percColour = Red
192                         }
193                         fmt.Fprintf(fd,
194                                 "%5d %8s %3d%% | %s%s%s\n",
195                                 n, humanize.IBytes(uint64(f.Length())), done,
196                                 percColour, f.Path(), Reset,
197                         )
198                 }
199                 fd.Close()
200                 time.Sleep(time.Second)
201         }
202 }
203
204 func fifoDHTList(c *torrent.Client) {
205         pth := path.Join(FIFOsDir, "dht")
206         recreateFIFO(pth)
207         for {
208                 fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
209                 if err != nil {
210                         if os.IsNotExist(err) {
211                                 break
212                         }
213                         log.Println("OpenFile:", pth, err)
214                         time.Sleep(time.Second)
215                         continue
216                 }
217                 for _, s := range c.DhtServers() {
218                         stats := s.Stats().(dht.ServerStats)
219                         fmt.Fprintf(
220                                 fd, "%s%s%s all:%d good:%d await:%d succ:%d bad:%d\n",
221                                 Green, s.Addr().String(), Reset,
222                                 stats.Nodes,
223                                 stats.GoodNodes,
224                                 stats.OutstandingTransactions,
225                                 stats.SuccessfulOutboundAnnouncePeerQueries,
226                                 stats.BadNodes,
227                         )
228                 }
229                 fd.Close()
230                 time.Sleep(time.Second)
231         }
232 }
233
234 type topTorrent struct {
235         infoHash metainfo.Hash
236         name     string
237         tx       int64
238         ratio    float64
239 }
240
241 func fifoTopSeed(c *torrent.Client) {
242         pth := path.Join(FIFOsDir, "top-seed")
243         recreateFIFO(pth)
244         for {
245                 fd, err := os.OpenFile(pth, os.O_WRONLY|os.O_APPEND, os.FileMode(0666))
246                 if err != nil {
247                         log.Println("OpenFile:", pth, err)
248                         time.Sleep(time.Second)
249                         continue
250                 }
251                 var ts []*topTorrent
252                 for _, t := range c.Torrents() {
253                         if t.Info() == nil {
254                                 continue
255                         }
256                         stats := t.Stats()
257                         top := topTorrent{
258                                 infoHash: t.InfoHash(),
259                                 name:     t.Name(),
260                                 tx:       stats.BytesWrittenData.Int64() + TxStats[t.InfoHash()],
261                         }
262                         top.ratio = float64(top.tx) / float64(t.Length())
263                         ts = append(ts, &top)
264                 }
265                 sort.Sort(ByTxTraffic(ts))
266                 for _, t := range ts {
267                         fmt.Fprintf(fd,
268                                 "%s%s%s %s%40s%s %s%4.1f%s %s\n",
269                                 Blue, t.infoHash.HexString(), Reset,
270                                 Green, shortenName(t.name), Reset,
271                                 Magenta, t.ratio, Reset,
272                                 humanize.IBytes(uint64(t.tx)),
273                         )
274                 }
275                 fd.Close()
276                 time.Sleep(time.Second)
277         }
278 }
279
280 type stringAddr string
281
282 func (stringAddr) Network() string   { return "" }
283 func (me stringAddr) String() string { return string(me) }
284
285 func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) {
286         for _, ta := range addrs {
287                 ret = append(ret, torrent.PeerInfo{Addr: stringAddr(ta)})
288         }
289         return
290 }
291
292 func readLinesFromFIFO(pth string) []string {
293         fd, err := os.OpenFile(pth, os.O_RDONLY, os.FileMode(0666))
294         if err != nil {
295                 log.Println("OpenFile:", pth, err)
296                 time.Sleep(time.Second)
297                 return nil
298         }
299         var lines []string
300         scanner := bufio.NewScanner(fd)
301         for scanner.Scan() {
302                 t := scanner.Text()
303                 if len(t) > 0 {
304                         lines = append(lines, t)
305                 }
306         }
307         fd.Close()
308         return lines
309 }
310
311 func saveTorrent(t *torrent.Torrent) error {
312         pth := storage.PathShortener(t.Name()) + TorrentExt
313         if _, err := os.Stat(pth); err == nil {
314                 return nil
315         }
316         var b bytes.Buffer
317         t.Metainfo().Write(&b)
318         return os.WriteFile(pth, b.Bytes(), 0666)
319 }
320
321 func fifoAdd(c *torrent.Client) {
322         pth := path.Join(FIFOsDir, "add")
323         recreateFIFO(pth)
324         for {
325                 for _, what := range readLinesFromFIFO(pth) {
326                         cols := strings.Fields(what)
327                         what = cols[0]
328                         var t *torrent.Torrent
329                         var err error
330                         if strings.HasPrefix(what, "magnet:") {
331                                 t, err = c.AddMagnet(what)
332                                 if err != nil {
333                                         log.Println("AddMagnet:", what, err)
334                                         continue
335                                 }
336                         } else {
337                                 metaInfo, err := metainfo.LoadFromFile(what)
338                                 if err != nil {
339                                         log.Println("LoadFromFile:", what, err)
340                                         continue
341                                 }
342                                 t, err = c.AddTorrent(metaInfo)
343                                 if err != nil {
344                                         log.Println("AddTorrent:", what, err)
345                                         continue
346                                 }
347                         }
348                         if len(cols) > 1 {
349                                 t.AddPeers(resolveTestPeers(cols[1:]))
350                         }
351                         TorrentsM.Lock()
352                         for _, h := range Torrents {
353                                 if h.HexString() == t.InfoHash().HexString() {
354                                         goto OldOne
355                                 }
356                         }
357                         Torrents = append(Torrents, t.InfoHash())
358                 OldOne:
359                         TorrentsM.Unlock()
360                         go fifoPeerList(t)
361                         go fifoFileList(t)
362                         log.Println("added:", t.InfoHash().HexString(), t.Name())
363                         go func() {
364                                 <-t.GotInfo()
365                                 if err = saveTorrent(t); err != nil {
366                                         log.Println("saveTorrent:", err)
367                                 }
368                                 txStatsLoad(t.InfoHash())
369                                 t.DownloadAll()
370                         }()
371                 }
372                 time.Sleep(time.Second)
373         }
374 }
375
376 func fifoDel(c *torrent.Client) {
377         pth := path.Join(FIFOsDir, "del")
378         recreateFIFO(pth)
379         for {
380                 for _, what := range readLinesFromFIFO(pth) {
381                         raw, err := hex.DecodeString(what)
382                         if err != nil {
383                                 log.Println(err)
384                                 continue
385                         }
386                         if len(raw) != infohash.Size {
387                                 log.Println("bad length")
388                                 continue
389                         }
390                         var i infohash.T
391                         copy(i[:], raw)
392                         TorrentsM.Lock()
393                         for n, h := range Torrents {
394                                 if h.HexString() == i.HexString() {
395                                         Torrents = append(Torrents[:n], Torrents[n+1:]...)
396                                         break
397                                 }
398                         }
399                         TorrentsM.Unlock()
400                         t, ok := c.Torrent(i)
401                         if !ok {
402                                 log.Println("no such torrent", what)
403                                 continue
404                         }
405                         txStatsDump(t)
406                         txStatsDel(t.InfoHash())
407                         t.Drop()
408                         for _, where := range []string{"files", "peers"} {
409                                 pth := path.Join(where, t.InfoHash().HexString())
410                                 os.Remove(pth)
411                                 fd, err := os.Open(pth)
412                                 if err == nil {
413                                         fd.Close()
414                                 }
415                         }
416                         log.Println("deleted:", what, t.Name())
417                 }
418                 time.Sleep(time.Second)
419         }
420 }
421
422 func fifosPrepare() {
423         os.MkdirAll(path.Join(FIFOsDir, PeersDir), 0777)
424         os.MkdirAll(path.Join(FIFOsDir, FilesDir), 0777)
425 }
426
427 func fifosCleanup() {
428         os.Remove(path.Join(FIFOsDir, "list"))
429         os.Remove(path.Join(FIFOsDir, "dht"))
430         os.Remove(path.Join(FIFOsDir, "add"))
431         os.Remove(path.Join(FIFOsDir, "del"))
432         os.Remove(path.Join(FIFOsDir, "top-seed"))
433         os.RemoveAll(path.Join(FIFOsDir, PeersDir))
434         os.RemoveAll(path.Join(FIFOsDir, FilesDir))
435 }