f513fca6a7
1. Fixed a bug, which reads certs even TLS is disabled 2. Persists Stratum TCP connection with auto-reconnect. (retry backoff increases from 1s to 60s maximum) 3. Stratum update jobs on previous notifications are canceled when a new notification arrives. Usually, the jobs are so short and completed immediately. However, if the Stratum connection is broken, this prevents the bridge from accumulating stale jobs.
172 lines
3.5 KiB
Go
172 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
type bridge struct {
|
|
ctx context.Context
|
|
|
|
prevJobContext context.Context
|
|
prevJobCancel context.CancelFunc
|
|
|
|
eventCh chan interface{}
|
|
errorc chan error
|
|
wg sync.WaitGroup
|
|
|
|
stratum *stratumClient
|
|
|
|
customCmd string
|
|
}
|
|
|
|
func newBridge(stratumServer, stratumPass, coinid string) *bridge {
|
|
|
|
s := &bridge{
|
|
ctx: context.Background(),
|
|
eventCh: make(chan interface{}),
|
|
errorc: make(chan error),
|
|
}
|
|
|
|
if len(stratumServer) > 0 {
|
|
s.stratum = newStratumClient(stratumServer, stratumPass, coinid)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (b *bridge) start() {
|
|
|
|
if b.stratum != nil {
|
|
backoff := time.Second
|
|
for {
|
|
err := b.stratum.dial()
|
|
if err == nil {
|
|
break
|
|
}
|
|
log.Printf("WARN: stratum.dial() error: %s, retry in %s", err, backoff)
|
|
time.Sleep(backoff)
|
|
if backoff < 60*time.Second {
|
|
backoff += time.Second
|
|
}
|
|
}
|
|
}
|
|
|
|
for e := range b.eventCh {
|
|
switch e := e.(type) {
|
|
case *eventBlockConected:
|
|
b.handleFilteredBlockConnected(e)
|
|
default:
|
|
b.errorc <- fmt.Errorf("unknown event type: %T", e)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *bridge) handleFilteredBlockConnected(e *eventBlockConected) {
|
|
|
|
if !*quiet {
|
|
log.Printf("Block connected: %s (%d) %v", e.header.BlockHash(), e.height, e.header.Timestamp)
|
|
}
|
|
|
|
hash := e.header.BlockHash().String()
|
|
height := e.height
|
|
|
|
// Cancel jobs on previous block. It's safe if they are already done.
|
|
if b.prevJobContext != nil {
|
|
select {
|
|
case <-b.prevJobContext.Done():
|
|
log.Printf("prev one canceled")
|
|
default:
|
|
b.prevJobCancel()
|
|
}
|
|
}
|
|
|
|
// Wait until all previous jobs are done or canceled.
|
|
b.wg.Wait()
|
|
|
|
// Create and save cancelable subcontext for new jobs.
|
|
ctx, cancel := context.WithCancel(b.ctx)
|
|
b.prevJobContext, b.prevJobCancel = ctx, cancel
|
|
|
|
if len(b.customCmd) > 0 {
|
|
go b.execCustomCommand(ctx, hash, height)
|
|
}
|
|
|
|
// Send stratum update block message
|
|
if b.stratum != nil {
|
|
go b.stratumUpdateBlock(ctx, hash, height)
|
|
}
|
|
}
|
|
|
|
func (s *bridge) stratumUpdateBlock(ctx context.Context, hash string, height int32) {
|
|
s.wg.Add(1)
|
|
defer s.wg.Done()
|
|
|
|
backoff := time.Second
|
|
retry := func(err error) {
|
|
if backoff < 60*time.Second {
|
|
backoff += time.Second
|
|
}
|
|
log.Printf("WARN: stratum.send() on block %d error: %s", height, err)
|
|
time.Sleep(backoff)
|
|
s.stratum.dial()
|
|
}
|
|
|
|
msg := stratumUpdateBlockMsg(*stratumPass, *coinid, hash)
|
|
|
|
for {
|
|
switch err := s.stratum.send(ctx, msg); {
|
|
case err == nil:
|
|
return
|
|
case errors.Is(err, context.Canceled):
|
|
log.Printf("INFO: stratum.send() on block %d: %s.", height, err)
|
|
return
|
|
case errors.Is(err, syscall.EPIPE):
|
|
errClose := s.stratum.conn.Close()
|
|
if errClose != nil {
|
|
log.Printf("WARN: stratum.conn.Close() on block %d: %s.", height, errClose)
|
|
}
|
|
retry(err)
|
|
case errors.Is(err, net.ErrClosed):
|
|
retry(err)
|
|
default:
|
|
retry(err)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (s *bridge) execCustomCommand(ctx context.Context, hash string, height int32) {
|
|
s.wg.Add(1)
|
|
defer s.wg.Done()
|
|
|
|
cmd := strings.ReplaceAll(s.customCmd, "%s", hash)
|
|
err := doExecCustomCommand(ctx, cmd)
|
|
if err != nil {
|
|
log.Printf("ERROR: execCustomCommand on block %s(%d): %s", hash, height, err)
|
|
}
|
|
}
|
|
|
|
func doExecCustomCommand(ctx context.Context, cmd string) error {
|
|
strs := strings.Split(cmd, " ")
|
|
path, err := exec.LookPath(strs[0])
|
|
if errors.Is(err, exec.ErrDot) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c := exec.CommandContext(ctx, path, strs[1:]...)
|
|
c.Stdout = os.Stdout
|
|
return c.Run()
|
|
}
|