fix: comprehensive bug fixes and architecture restructuring
Phase 1 — Bug fixes (7 bugs): - Bug 1: Pulsar mode data loss — always persist to SQLite regardless of mode - Bug 2: PulsarSend closing TCP client — use independent resetPulsarProducer() - Bug 3: Greedy regex — use non-greedy (?s)ZCZC.*?NNNN - Bug 4: Data after NNNN discarded — keep remaining buffer data - Bug 5: strings.Index > 0 boundary — use strings.Contains - Bug 6: Variable shadowing in PulsarSend — use = not := - Bug 7: Accept failure nil panic — add continue + retry logic Phase 2 — Architecture restructuring: - Split utils/ into config/, serial/, telegram/, storage/, transport/ - Define Sender, Repository, Reader interfaces - Introduce app/ layer with context.Context lifecycle - Replace spinlock with sync.Mutex - Unified Config struct replaces 15+ global vars Phase 3 — Testing & tooling: - telegram/parser_test.go (7 test cases) - storage/store_test.go (5 test cases) - transport/transport_test.go (4 test cases) - Taskfile: add test, test-race, test-cover tasks - Go version: 1.15 -> 1.21
This commit is contained in:
+150
@@ -0,0 +1,150 @@
|
||||
# https://taskfile.dev
|
||||
version: "3"
|
||||
|
||||
vars:
|
||||
APP: tele-recv
|
||||
BIN_DIR: bin
|
||||
CONFIG: telegram.yaml
|
||||
GO_VERSION: "1.15"
|
||||
LDFLAGS: '-s -w'
|
||||
BUILD_DIR: "{{.BIN_DIR}}/{{.APP}}"
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: Show available tasks
|
||||
cmd: task --list-all
|
||||
|
||||
# ─── Build ────────────────────────────────────────────────────────────────────
|
||||
|
||||
build:
|
||||
desc: Build the binary for the current platform
|
||||
deps: [deps]
|
||||
cmds:
|
||||
- mkdir -p "{{.BIN_DIR}}"
|
||||
- CGO_ENABLED=1 go build -ldflags="{{.LDFLAGS}}" -o "{{.BIN_DIR}}/{{.APP}}" .
|
||||
sources:
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
generates:
|
||||
- "{{.BIN_DIR}}/{{.APP}}"
|
||||
|
||||
build-all:
|
||||
desc: Cross-compile for linux/amd64 and windows/amd64
|
||||
cmds:
|
||||
- task: build-linux
|
||||
- task: build-windows
|
||||
|
||||
build-linux:
|
||||
desc: Build for linux/amd64 (requires CGO cross-compiler)
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: "1"
|
||||
CC: x86_64-linux-gnu-gcc
|
||||
cmds:
|
||||
- mkdir -p "{{.BIN_DIR}}"
|
||||
- go build -ldflags="{{.LDFLAGS}}" -o "{{.BIN_DIR}}/{{.APP}}-linux" .
|
||||
sources:
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
generates:
|
||||
- "{{.BIN_DIR}}/{{.APP}}-linux"
|
||||
silent: false
|
||||
|
||||
build-windows:
|
||||
desc: Build for windows/amd64 (requires MinGW cross-compiler)
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: "1"
|
||||
CC: x86_64-w64-mingw32-gcc
|
||||
cmds:
|
||||
- mkdir -p "{{.BIN_DIR}}"
|
||||
- go build -ldflags="{{.LDFLAGS}}" -o "{{.BIN_DIR}}/{{.APP}}-win64.exe" .
|
||||
sources:
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
generates:
|
||||
- "{{.BIN_DIR}}/{{.APP}}-win64.exe"
|
||||
silent: false
|
||||
|
||||
# ─── Test ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
test:
|
||||
desc: Run all tests
|
||||
cmds:
|
||||
- go test ./...
|
||||
|
||||
test-race:
|
||||
desc: Run tests with race detector
|
||||
cmds:
|
||||
- go test -race ./...
|
||||
|
||||
test-cover:
|
||||
desc: Run tests with coverage report
|
||||
cmds:
|
||||
- go test -coverprofile=coverage.out ./...
|
||||
- go tool cover -func=coverage.out
|
||||
|
||||
# ─── Run ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
run:
|
||||
desc: Run the service (start subcommand)
|
||||
deps: [build]
|
||||
cmds:
|
||||
- "{{.BIN_DIR}}/{{.APP}} start"
|
||||
|
||||
run-test:
|
||||
desc: Run the environment test (test subcommand)
|
||||
deps: [build]
|
||||
cmds:
|
||||
- "{{.BIN_DIR}}/{{.APP}} test"
|
||||
|
||||
# ─── Development helpers ──────────────────────────────────────────────────────
|
||||
|
||||
emu:
|
||||
desc: Create a virtual serial port pair (ttyS0 ↔ ttyS1) with socat
|
||||
cmds:
|
||||
- socat PTY,link=ttyS0 PTY,link=ttyS1
|
||||
silent: false
|
||||
|
||||
deps:
|
||||
desc: Tidy and download Go module dependencies
|
||||
cmds:
|
||||
- go mod tidy
|
||||
- go mod download
|
||||
sources:
|
||||
- go.mod
|
||||
- go.sum
|
||||
generates:
|
||||
- go.sum
|
||||
|
||||
lint:
|
||||
desc: Run go vet on all packages
|
||||
cmds:
|
||||
- go vet ./...
|
||||
|
||||
clean:
|
||||
desc: Remove build artifacts, logs, and database
|
||||
cmds:
|
||||
- rm -rf "{{.BIN_DIR}}"
|
||||
- rm -f telegram.db
|
||||
- rm -rf ./logs/
|
||||
silent: false
|
||||
|
||||
# ─── Distribution package ─────────────────────────────────────────────────────
|
||||
|
||||
dist:
|
||||
desc: Build all platforms and package into a tarball
|
||||
cmds:
|
||||
- task: build-all
|
||||
- mkdir -p "{{.BUILD_DIR}}"
|
||||
- cp "{{.BIN_DIR}}/{{.APP}}-linux" "{{.BUILD_DIR}}/"
|
||||
- cp "{{.BIN_DIR}}/{{.APP}}-win64.exe" "{{.BUILD_DIR}}/"
|
||||
- cp "{{.CONFIG}}" "{{.BUILD_DIR}}/"
|
||||
- cd "{{.BIN_DIR}}" && tar czvf "{{.APP}}.tar.gz" "{{.APP}}/" && mv "{{.APP}}.tar.gz" .
|
||||
- rm -rf "{{.BUILD_DIR}}"
|
||||
silent: false
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
|
||||
|
||||
"it2000.com.cn/tele-recv/config"
|
||||
"it2000.com.cn/tele-recv/serial"
|
||||
"it2000.com.cn/tele-recv/telegram"
|
||||
"it2000.com.cn/tele-recv/storage"
|
||||
"it2000.com.cn/tele-recv/transport"
|
||||
)
|
||||
|
||||
// App orchestrates the complete telegram receive pipeline.
|
||||
type App struct {
|
||||
cfg *config.Config
|
||||
logger *zap.Logger
|
||||
rawLog *rotatelogs.RotateLogs
|
||||
port *serial.Port
|
||||
parser *telegram.Parser
|
||||
store *storage.Store
|
||||
sender transport.Sender
|
||||
tcpSrv *transport.TCPServer
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// New creates a new App from configuration.
|
||||
func New(cfg *config.Config) (*App, error) {
|
||||
// Initialize logger
|
||||
logger, rawLog, err := initLoggers(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init log: %w", err)
|
||||
}
|
||||
|
||||
// Initialize parser
|
||||
parser := telegram.New(rawLog)
|
||||
|
||||
// Initialize store
|
||||
store, err := storage.New(cfg.SQLite.File, cfg.SQLite.Init)
|
||||
if err != nil {
|
||||
logger.Error("failed to init store", zap.Error(err))
|
||||
// Non-fatal: we can still run without persistence
|
||||
store = nil
|
||||
}
|
||||
|
||||
// Initialize sender
|
||||
var sender transport.Sender
|
||||
var tcpSrv *transport.TCPServer
|
||||
|
||||
if cfg.Telegram.TCP {
|
||||
tcpSender := transport.NewTCPSender(cfg.Socket.Address)
|
||||
tcpSrv = transport.NewTCPServer(cfg.Socket.Address, tcpSender, nil)
|
||||
sender = tcpSender
|
||||
}
|
||||
|
||||
if cfg.Telegram.Pulsar {
|
||||
pulsarSender, err := transport.NewPulsarSender(
|
||||
cfg.Pulsar.URL, cfg.Pulsar.Topic, cfg.Pulsar.Name,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Warn("failed to init pulsar sender", zap.Error(err))
|
||||
} else {
|
||||
if sender != nil {
|
||||
sender = transport.NewMultiSender(sender, pulsarSender)
|
||||
} else {
|
||||
sender = pulsarSender
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &App{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
rawLog: rawLog,
|
||||
parser: parser,
|
||||
store: store,
|
||||
sender: sender,
|
||||
tcpSrv: tcpSrv,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run starts the application and blocks until a signal or error.
|
||||
func (a *App) Run() error {
|
||||
defer a.logger.Sync()
|
||||
defer a.cleanup()
|
||||
|
||||
a.logger.Info("starting telegram receiver",
|
||||
zap.String("device", a.cfg.Serial.Device),
|
||||
zap.Int("baudrate", a.cfg.Serial.Baudrate))
|
||||
|
||||
// Start TCP server if configured
|
||||
if a.tcpSrv != nil {
|
||||
a.wg.Add(1)
|
||||
go func() {
|
||||
defer a.wg.Done()
|
||||
if err := a.tcpSrv.Run(a.ctx); err != nil {
|
||||
a.logger.Error("tcp server error", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Signal handling
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Main loop: read from serial port, parse, store, send
|
||||
const maxRetries = 3
|
||||
runCtx, runCancel := context.WithCancel(a.ctx)
|
||||
defer runCancel()
|
||||
|
||||
go func() {
|
||||
<-sigCh
|
||||
a.logger.Info("received signal, shutting down")
|
||||
runCancel()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-runCtx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
if a.port == nil || !a.port.IsOpen() {
|
||||
if err := a.openPort(maxRetries); err != nil {
|
||||
if err == context.Canceled {
|
||||
return nil
|
||||
}
|
||||
a.logger.Fatal("failed to open serial port", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
line, err := a.port.ReadLine()
|
||||
if err != nil {
|
||||
a.logger.Warn("serial read error", zap.Error(err))
|
||||
a.port.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
if a.cfg.Serial.LogRaw && len(line) > 0 {
|
||||
fmt.Println(line)
|
||||
}
|
||||
|
||||
telegrams := a.parser.Append(line)
|
||||
for _, telegram := range telegrams {
|
||||
a.dispatch(telegram)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) openPort(maxRetries int) error {
|
||||
a.logger.Info("opening serial port",
|
||||
zap.String("device", a.cfg.Serial.Device),
|
||||
zap.Int("baudrate", a.cfg.Serial.Baudrate))
|
||||
|
||||
var lastErr error
|
||||
for i := 1; i <= maxRetries; i++ {
|
||||
port, err := serial.Open(a.cfg.Serial.Device, a.cfg.Serial.Baudrate)
|
||||
if err == nil {
|
||||
a.port = port
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
a.logger.Warn("serial port open failed, retrying",
|
||||
zap.Int("attempt", i),
|
||||
zap.Error(err))
|
||||
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
return context.Canceled
|
||||
case <-time.After(time.Duration(1<<(i-1)) * time.Second):
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (a *App) dispatch(telegram string) {
|
||||
// Always persist first
|
||||
if a.store != nil {
|
||||
if err := a.store.Insert(telegram); err != nil {
|
||||
a.logger.Error("failed to persist telegram", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Then send to transport(s)
|
||||
if a.sender != nil {
|
||||
if err := a.sender.Send(telegram); err != nil {
|
||||
a.logger.Error("failed to send telegram", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) cleanup() {
|
||||
a.logger.Info("shutting down")
|
||||
|
||||
if a.tcpSrv != nil {
|
||||
a.tcpSrv.Stop()
|
||||
}
|
||||
|
||||
if a.port != nil {
|
||||
a.port.Close()
|
||||
}
|
||||
|
||||
if a.sender != nil {
|
||||
a.sender.Close()
|
||||
}
|
||||
|
||||
if a.store != nil {
|
||||
a.store.Close()
|
||||
}
|
||||
|
||||
a.wg.Wait()
|
||||
}
|
||||
|
||||
func initLoggers(cfg *config.Config) (*zap.Logger, *rotatelogs.RotateLogs, error) {
|
||||
logFile := cfg.Log.Dir + "/telegram-%Y-%m-%d-%H.log"
|
||||
rotator, err := rotatelogs.New(
|
||||
logFile,
|
||||
rotatelogs.WithMaxAge(time.Duration(cfg.Log.MaxAge)*24*time.Hour),
|
||||
rotatelogs.WithRotationTime(time.Duration(cfg.Log.RotateHour)*time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rawFile := cfg.Log.Dir + "/raw-%Y-%m-%d-%H.txt"
|
||||
rawLog, err := rotatelogs.New(
|
||||
rawFile,
|
||||
rotatelogs.WithMaxAge(time.Duration(cfg.Log.MaxAge)*24*time.Hour),
|
||||
rotatelogs.WithRotationTime(time.Duration(cfg.Log.RotateHour)*time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
rotator.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
filePriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||
return lvl >= zapcore.DebugLevel
|
||||
})
|
||||
stdoutPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||
return lvl >= zapcore.InfoLevel
|
||||
})
|
||||
|
||||
encoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
|
||||
core := zapcore.NewTee(
|
||||
zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), stdoutPriority),
|
||||
zapcore.NewCore(encoder, zapcore.AddSync(rotator), filePriority),
|
||||
)
|
||||
|
||||
logger := zap.New(core)
|
||||
|
||||
return logger, rawLog, nil
|
||||
}
|
||||
+35
-59
@@ -25,43 +25,39 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"it2000.com.cn/tele-recv/config"
|
||||
"it2000.com.cn/tele-recv/utils"
|
||||
)
|
||||
|
||||
const ModeName = "TELEGRAM_MODE"
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
device string
|
||||
baudrate int
|
||||
lograw bool
|
||||
|
||||
dbFile string
|
||||
dbInit bool
|
||||
cfgFile string
|
||||
|
||||
// Legacy global vars for backward compatibility with test command
|
||||
device string
|
||||
baudrate int
|
||||
dbFile string
|
||||
dbInit bool
|
||||
socketAddress string
|
||||
pulsarUrl string
|
||||
topic string
|
||||
name string
|
||||
tcp bool
|
||||
pulsar bool
|
||||
lograw bool
|
||||
|
||||
pulsarUrl string
|
||||
topic string
|
||||
name string
|
||||
|
||||
tcp bool
|
||||
pulsar bool
|
||||
Mode string
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "tele-recv",
|
||||
Short: "telegram receiver",
|
||||
Long: `A serial port telegram receiver.
|
||||
Read the telegram from `,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
// Run: func(cmd *cobra.Command, args []string) { },
|
||||
Read the telegram from serial port`,
|
||||
}
|
||||
|
||||
Mode = os.Getenv(ModeName)
|
||||
|
||||
appCfg *config.Config
|
||||
logger *zap.Logger
|
||||
)
|
||||
|
||||
@@ -77,55 +73,36 @@ func Execute() {
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (telegram.yaml)")
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
|
||||
initLog()
|
||||
defer logger.Sync()
|
||||
// l = logger.Sugar()
|
||||
utils.Log = logger
|
||||
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Search config in home directory with name ".tele-recv" (without extension).
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName("telegram")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
// fmt.Println("Using config file:", viper.ConfigFileUsed())
|
||||
logger.Info("Using config file ", zap.String("path", viper.ConfigFileUsed()))
|
||||
device = viper.GetString("serial.device")
|
||||
baudrate = viper.GetInt("serial.baudrate")
|
||||
lograw = viper.GetBool("serial.lograw")
|
||||
dbFile = viper.GetString("sqlite.file")
|
||||
dbInit = viper.GetBool("sqlite.init")
|
||||
socketAddress = viper.GetString("socket.address")
|
||||
pulsarUrl = viper.GetString("pulsar.url")
|
||||
topic = viper.GetString("pulsar.topic")
|
||||
name = viper.GetString("pulsar.name")
|
||||
|
||||
tcp = viper.GetBool("telegram.tcp")
|
||||
utils.Tcp = tcp
|
||||
pulsar = viper.GetBool("telegram.pulsar")
|
||||
utils.Pulsar = pulsar
|
||||
cfg, err := config.Load(cfgFile)
|
||||
if err != nil {
|
||||
logger.Warn("config load error, using defaults", zap.Error(err))
|
||||
return
|
||||
}
|
||||
appCfg = cfg
|
||||
|
||||
// Populate legacy globals for backward compatibility
|
||||
device = cfg.Serial.Device
|
||||
baudrate = cfg.Serial.Baudrate
|
||||
lograw = cfg.Serial.LogRaw
|
||||
dbFile = cfg.SQLite.File
|
||||
dbInit = cfg.SQLite.Init
|
||||
socketAddress = cfg.Socket.Address
|
||||
pulsarUrl = cfg.Pulsar.URL
|
||||
topic = cfg.Pulsar.Topic
|
||||
name = cfg.Pulsar.Name
|
||||
tcp = cfg.Telegram.TCP
|
||||
pulsar = cfg.Telegram.Pulsar
|
||||
utils.Tcp = tcp
|
||||
utils.Pulsar = pulsar
|
||||
}
|
||||
|
||||
func initLog() {
|
||||
@@ -163,5 +140,4 @@ func initLog() {
|
||||
)
|
||||
|
||||
logger = zap.New(core)
|
||||
|
||||
}
|
||||
|
||||
+14
-80
@@ -16,15 +16,10 @@ limitations under the License.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"it2000.com.cn/tele-recv/utils"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"it2000.com.cn/tele-recv/app"
|
||||
)
|
||||
|
||||
// startCmd represents the start command
|
||||
@@ -33,85 +28,24 @@ var startCmd = &cobra.Command{
|
||||
Short: "start telegram receive service",
|
||||
Long: `open serial port
|
||||
start a tcp server for processing`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
start()
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return start()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(startCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// startCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
|
||||
func start() {
|
||||
// Go signal notification works by sending `os.Signal`
|
||||
// values on a channel. We'll create a channel to
|
||||
// receive these notifications (we'll also make one to
|
||||
// notify us when the program can exit).
|
||||
sigs := make(chan os.Signal, 1)
|
||||
done := make(chan bool, 1)
|
||||
|
||||
// `signal.Notify` registers the given channel to
|
||||
// receive notifications of the specified signals.
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
utils.ServerRunning = true
|
||||
|
||||
// This goroutine executes a blocking receive for
|
||||
// signals. When it gets one it'll print it out
|
||||
// and then notify the program that it can finish.
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
logger.Info("got ", zap.Any("signal", sig))
|
||||
utils.ServerRunning = false
|
||||
if tcp {
|
||||
utils.StopSocketServer()
|
||||
}
|
||||
if pulsar {
|
||||
utils.ClosePulsar()
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// The program will wait here until it gets the
|
||||
// expected signal (as indicated by the goroutine
|
||||
// above sending a value on `done`) and then exit.
|
||||
logger.Info("awaiting signal")
|
||||
_ = utils.InitDb(dbFile, dbInit)
|
||||
if tcp {
|
||||
go utils.Listen(socketAddress)
|
||||
func start() error {
|
||||
if appCfg == nil {
|
||||
logger.Fatal("configuration not loaded")
|
||||
}
|
||||
if pulsar {
|
||||
utils.CreateProducer(pulsarUrl, topic, name)
|
||||
}
|
||||
for utils.ServerRunning {
|
||||
if !utils.IsPortOpen() {
|
||||
logger.Info("try to open port")
|
||||
err := utils.OpenPort(device, baudrate)
|
||||
if err != nil {
|
||||
logger.Fatal("error in open serial port ", zap.Error(err))
|
||||
}
|
||||
logger.Info("starting read")
|
||||
}
|
||||
buffer, err := utils.ReadPort()
|
||||
if err == nil {
|
||||
if lograw && len(buffer) > 0 {
|
||||
fmt.Println(buffer)
|
||||
}
|
||||
if utils.Append(buffer) {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
<-done
|
||||
logger.Info("exiting")
|
||||
|
||||
a, err := app.New(appCfg)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to create app", zap.Error(err))
|
||||
}
|
||||
|
||||
return a.Run()
|
||||
}
|
||||
|
||||
-10
@@ -47,14 +47,4 @@ func test() {
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(testCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// testCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// testCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
// Config holds all application configuration.
|
||||
type Config struct {
|
||||
Serial SerialConfig
|
||||
Telegram TelegramConfig
|
||||
SQLite SQLiteConfig
|
||||
Socket SocketConfig
|
||||
Pulsar PulsarConfig
|
||||
Log LogConfig
|
||||
}
|
||||
|
||||
type SerialConfig struct {
|
||||
Device string
|
||||
Baudrate int
|
||||
LogRaw bool
|
||||
}
|
||||
|
||||
type TelegramConfig struct {
|
||||
TCP bool
|
||||
Pulsar bool
|
||||
}
|
||||
|
||||
type SQLiteConfig struct {
|
||||
File string
|
||||
Init bool
|
||||
}
|
||||
|
||||
type SocketConfig struct {
|
||||
Address string
|
||||
}
|
||||
|
||||
type PulsarConfig struct {
|
||||
URL string
|
||||
Topic string
|
||||
Name string
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Dir string
|
||||
MaxAge int // days
|
||||
RotateHour int
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Load reads configuration from telegram.yaml using viper.
|
||||
func Load(path string) (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
if path != "" {
|
||||
v.SetConfigFile(path)
|
||||
} else {
|
||||
v.AddConfigPath(".")
|
||||
v.SetConfigName("telegram")
|
||||
}
|
||||
|
||||
v.AutomaticEnv()
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
Serial: SerialConfig{
|
||||
Device: v.GetString("serial.device"),
|
||||
Baudrate: v.GetInt("serial.baudrate"),
|
||||
LogRaw: v.GetBool("serial.lograw"),
|
||||
},
|
||||
Telegram: TelegramConfig{
|
||||
TCP: v.GetBool("telegram.tcp"),
|
||||
Pulsar: v.GetBool("telegram.pulsar"),
|
||||
},
|
||||
SQLite: SQLiteConfig{
|
||||
File: v.GetString("sqlite.file"),
|
||||
Init: v.GetBool("sqlite.init"),
|
||||
},
|
||||
Socket: SocketConfig{
|
||||
Address: v.GetString("socket.address"),
|
||||
},
|
||||
Pulsar: PulsarConfig{
|
||||
URL: v.GetString("pulsar.url"),
|
||||
Topic: v.GetString("pulsar.topic"),
|
||||
Name: v.GetString("pulsar.name"),
|
||||
},
|
||||
Log: LogConfig{
|
||||
Dir: "./logs",
|
||||
MaxAge: 60,
|
||||
RotateHour: 1,
|
||||
},
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -1,28 +1,72 @@
|
||||
module it2000.com.cn/tele-recv
|
||||
|
||||
go 1.15
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/apache/pulsar-client-go v0.3.0
|
||||
github.com/argandas/serial v0.0.0-20160316175758-889a5ad85462
|
||||
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect
|
||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||
github.com/lestrrat-go/strftime v1.0.3 // indirect
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.5
|
||||
github.com/spf13/cobra v1.1.1
|
||||
github.com/spf13/viper v1.7.1
|
||||
go.uber.org/zap v1.16.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/99designs/keyring v1.1.5 // indirect
|
||||
github.com/apache/pulsar-client-go/oauth2 v0.0.0-20200715083626-b9f8c5cedefb // indirect
|
||||
github.com/ardielle/ardielle-go v1.5.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/danieljoos/wincred v1.0.2 // indirect
|
||||
github.com/datadog/zstd v1.4.6-0.20200617134701-89f69fb7df32 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a // indirect
|
||||
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
|
||||
github.com/gogo/protobuf v1.3.1 // indirect
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect
|
||||
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect
|
||||
github.com/klauspost/compress v1.10.8 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
|
||||
github.com/lestrrat-go/strftime v1.0.3 // indirect
|
||||
github.com/linkedin/goavro/v2 v2.9.8 // indirect
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.0 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.7.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.10.0 // indirect
|
||||
github.com/prometheus/procfs v0.1.3 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.5.1 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/cobra v1.1.1
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.7.1
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/tebeka/strftime v0.1.5 // indirect
|
||||
github.com/yahoo/athenz v1.8.55 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.16.0
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 // indirect
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e // indirect
|
||||
golang.org/x/text v0.3.4 // indirect
|
||||
google.golang.org/appengine v1.6.1 // indirect
|
||||
google.golang.org/protobuf v1.23.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -42,7 +42,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
||||
github.com/boynton/repl v0.0.0-20170116235056-348863958e3e/go.mod h1:Crc/GCZ3NXDVCio7Yr0o+SSrytpcFhLmVCIzi0s49t4=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -174,7 +173,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@@ -234,7 +232,6 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -475,7 +472,6 @@ google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyz
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package serial
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/argandas/serial"
|
||||
)
|
||||
|
||||
// Reader defines the interface for serial port operations.
|
||||
type Reader interface {
|
||||
ReadLine() (string, error)
|
||||
Close() error
|
||||
IsOpen() bool
|
||||
}
|
||||
|
||||
// Port wraps a serial port connection.
|
||||
type Port struct {
|
||||
sp *serial.SerialPort
|
||||
opened bool
|
||||
}
|
||||
|
||||
// Open opens a serial port with the given device and baudrate.
|
||||
func Open(device string, baudrate int) (*Port, error) {
|
||||
sp := serial.New()
|
||||
sp.EOL('\r')
|
||||
sp.Verbose = false
|
||||
|
||||
err := sp.Open(device, baudrate, 3*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Port{sp: sp, opened: true}, nil
|
||||
}
|
||||
|
||||
// ReadLine reads one line from the serial port.
|
||||
func (p *Port) ReadLine() (string, error) {
|
||||
return p.sp.ReadLine()
|
||||
}
|
||||
|
||||
// Close closes the serial port.
|
||||
func (p *Port) Close() error {
|
||||
p.opened = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsOpen returns whether the port is currently open.
|
||||
func (p *Port) IsOpen() bool {
|
||||
return p.opened
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Repository defines the interface for telegram persistence.
|
||||
type Repository interface {
|
||||
Insert(telegram string) error
|
||||
LoadUnprocessed() ([]Telegram, error)
|
||||
MarkProcessed(id int64) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Telegram represents a stored telegram record.
|
||||
type Telegram struct {
|
||||
ID int64
|
||||
Text string
|
||||
}
|
||||
|
||||
// Store implements Repository using SQLite.
|
||||
type Store struct {
|
||||
mu sync.Mutex
|
||||
dbFile string
|
||||
db *sql.DB
|
||||
initTable bool
|
||||
}
|
||||
|
||||
const (
|
||||
driverName = "sqlite3"
|
||||
tableDDL = `
|
||||
create table IF NOT EXISTS telegram (
|
||||
[tele_id] INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
[tele_recv_time] TIMESTAMP NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||||
[tele_processed] int(1) NOT NULL DEFAULT 0,
|
||||
[tele_text] TEXT NOT NULL
|
||||
)
|
||||
`
|
||||
insertSQL = "insert into telegram (tele_text) values (?)"
|
||||
countSQL = "select count(*) from telegram where tele_processed=0"
|
||||
loadSQL = "select tele_id, tele_text from telegram where tele_processed=0 Limit 100"
|
||||
updateSQL = "update telegram set tele_processed = 1 where tele_id=?"
|
||||
)
|
||||
|
||||
// New opens or creates a SQLite store.
|
||||
func New(dbFile string, init bool) (*Store, error) {
|
||||
if init {
|
||||
os.Remove(dbFile)
|
||||
}
|
||||
|
||||
db, err := sql.Open(driverName, dbFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := db.Exec(tableDDL); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Store{
|
||||
dbFile: dbFile,
|
||||
db: db,
|
||||
initTable: init,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Insert saves a telegram to the database.
|
||||
func (s *Store) Insert(telegram string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
stmt, err := s.db.Prepare(insertSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(telegram)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadUnprocessed returns up to 100 unprocessed telegrams.
|
||||
func (s *Store) LoadUnprocessed() ([]Telegram, error) {
|
||||
rows, err := s.db.Query(loadSQL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var telegrams []Telegram
|
||||
for rows.Next() {
|
||||
var t Telegram
|
||||
if err := rows.Scan(&t.ID, &t.Text); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
telegrams = append(telegrams, t)
|
||||
}
|
||||
return telegrams, nil
|
||||
}
|
||||
|
||||
// MarkProcessed marks a telegram as processed.
|
||||
func (s *Store) MarkProcessed(id int64) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
stmt, err := s.db.Prepare(updateSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(id)
|
||||
return err
|
||||
}
|
||||
|
||||
// CountUnprocessed returns the number of unprocessed telegrams.
|
||||
func (s *Store) CountUnprocessed() (int64, error) {
|
||||
var count int64
|
||||
err := s.db.QueryRow(countSQL).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStore_InsertAndCount(t *testing.T) {
|
||||
dbFile := "test_telegram.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
s, err := New(dbFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
if err := s.Insert("ZCZC TEST NNNN"); err != nil {
|
||||
t.Fatalf("Insert() failed: %v", err)
|
||||
}
|
||||
|
||||
count, err := s.CountUnprocessed()
|
||||
if err != nil {
|
||||
t.Fatalf("CountUnprocessed() failed: %v", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("expected 1 unprocessed, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_LoadUnprocessed(t *testing.T) {
|
||||
dbFile := "test_load.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
s, err := New(dbFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
s.Insert("ZCZC MSG1 NNNN")
|
||||
s.Insert("ZCZC MSG2 NNNN")
|
||||
|
||||
telegrams, err := s.LoadUnprocessed()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadUnprocessed() failed: %v", err)
|
||||
}
|
||||
if len(telegrams) != 2 {
|
||||
t.Errorf("expected 2 telegrams, got %d", len(telegrams))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_MarkProcessed(t *testing.T) {
|
||||
dbFile := "test_mark.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
s, err := New(dbFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
s.Insert("ZCZC TEST NNNN")
|
||||
|
||||
telegrams, _ := s.LoadUnprocessed()
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram, got %d", len(telegrams))
|
||||
}
|
||||
|
||||
if err := s.MarkProcessed(telegrams[0].ID); err != nil {
|
||||
t.Fatalf("MarkProcessed() failed: %v", err)
|
||||
}
|
||||
|
||||
count, _ := s.CountUnprocessed()
|
||||
if count != 0 {
|
||||
t.Errorf("expected 0 unprocessed after marking, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_Empty(t *testing.T) {
|
||||
dbFile := "test_empty.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
s, err := New(dbFile, true)
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
telegrams, err := s.LoadUnprocessed()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadUnprocessed() failed: %v", err)
|
||||
}
|
||||
if len(telegrams) != 0 {
|
||||
t.Errorf("expected 0 telegrams, got %d", len(telegrams))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
endTag = "NNNN"
|
||||
expression = "(?s)ZCZC.*?NNNN"
|
||||
maxBufferSize = 65536
|
||||
bufferTrimSize = 32768
|
||||
)
|
||||
|
||||
// Parser handles telegram extraction from raw serial data.
|
||||
type Parser struct {
|
||||
mu sync.Mutex
|
||||
buffer strings.Builder
|
||||
exp *regexp.Regexp
|
||||
rawLog io.Writer
|
||||
}
|
||||
|
||||
// New creates a new Parser.
|
||||
func New(rawLog io.Writer) *Parser {
|
||||
return &Parser{
|
||||
exp: regexp.MustCompile(expression),
|
||||
rawLog: rawLog,
|
||||
}
|
||||
}
|
||||
|
||||
// Append adds raw data to the internal buffer and returns any complete
|
||||
// telegrams that were found. Returns nil if no complete telegram is ready.
|
||||
func (p *Parser) Append(data string) []string {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.buffer.Len() > maxBufferSize {
|
||||
// Truncate to prevent unbounded growth
|
||||
existing := p.buffer.String()
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(existing[len(existing)-bufferTrimSize:])
|
||||
}
|
||||
|
||||
p.buffer.WriteString(data)
|
||||
p.buffer.WriteByte('\n')
|
||||
|
||||
return p.extract()
|
||||
}
|
||||
|
||||
// extract finds and returns all complete telegrams in the buffer.
|
||||
func (p *Parser) extract() []string {
|
||||
content := p.buffer.String()
|
||||
|
||||
if !strings.Contains(content, endTag) {
|
||||
return nil
|
||||
}
|
||||
|
||||
loc := p.exp.FindStringIndex(content)
|
||||
if loc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
telegram := content[loc[0]:loc[1]]
|
||||
telegram = removeEmpty(telegram) + "\n\n\n"
|
||||
|
||||
// Write raw log
|
||||
if p.rawLog != nil {
|
||||
p.rawLog.Write([]byte(telegram + "\n"))
|
||||
}
|
||||
|
||||
// Keep remaining data after the matched telegram
|
||||
remaining := content[loc[1]:]
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(remaining)
|
||||
|
||||
return []string{telegram}
|
||||
}
|
||||
|
||||
func removeEmpty(s string) string {
|
||||
return regexp.MustCompile(`[\t\r\n]+`).ReplaceAllString(strings.TrimSpace(s), "\n")
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParser_SingleTelegram(t *testing.T) {
|
||||
p := New(nil)
|
||||
telegrams := p.Append("ZCZC TEST MESSAGE NNNN")
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram, got %d", len(telegrams))
|
||||
}
|
||||
if !strings.Contains(telegrams[0], "ZCZC TEST MESSAGE NNNN") {
|
||||
t.Errorf("unexpected telegram content: %s", telegrams[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_MultipleTelegrams(t *testing.T) {
|
||||
p := New(nil)
|
||||
// Simulate two telegrams arriving in one burst
|
||||
telegrams := p.Append("ZCZC MSG1 NNNN ZCZC MSG2 NNNN")
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram (non-greedy stops at first NNNN), got %d", len(telegrams))
|
||||
}
|
||||
if !strings.Contains(telegrams[0], "ZCZC MSG1 NNNN") {
|
||||
t.Errorf("unexpected telegram content: %s", telegrams[0])
|
||||
}
|
||||
|
||||
// Second telegram should be in buffer
|
||||
telegrams = p.Append("")
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram from remaining buffer, got %d", len(telegrams))
|
||||
}
|
||||
if !strings.Contains(telegrams[0], "ZCZC MSG2 NNNN") {
|
||||
t.Errorf("unexpected telegram content: %s", telegrams[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_NNNNInBufferStart(t *testing.T) {
|
||||
p := New(nil)
|
||||
// Simulate leftover NNNN from previous truncation
|
||||
telegrams := p.Append("NNNN ZCZC TEST NNNN")
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram, got %d", len(telegrams))
|
||||
}
|
||||
if !strings.Contains(telegrams[0], "ZCZC TEST NNNN") {
|
||||
t.Errorf("unexpected telegram content: %s", telegrams[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_NoTelegram(t *testing.T) {
|
||||
p := New(nil)
|
||||
telegrams := p.Append("some random noise without markers")
|
||||
if len(telegrams) != 0 {
|
||||
t.Fatalf("expected 0 telegrams, got %d", len(telegrams))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_PartialTelegram(t *testing.T) {
|
||||
p := New(nil)
|
||||
// First line: start of telegram
|
||||
telegrams := p.Append("ZCZC PARTIAL")
|
||||
if len(telegrams) != 0 {
|
||||
t.Fatalf("expected 0 telegrams (incomplete), got %d", len(telegrams))
|
||||
}
|
||||
// Second line: completion
|
||||
telegrams = p.Append("CONTINUES HERE NNNN")
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram after completion, got %d", len(telegrams))
|
||||
}
|
||||
if !strings.Contains(telegrams[0], "ZCZC PARTIAL") || !strings.Contains(telegrams[0], "CONTINUES HERE NNNN") {
|
||||
t.Errorf("unexpected telegram content: %s", telegrams[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_BufferOverflow(t *testing.T) {
|
||||
p := New(nil)
|
||||
// Fill buffer beyond maxBufferSize
|
||||
largeData := strings.Repeat("A", maxBufferSize+100)
|
||||
telegrams := p.Append(largeData)
|
||||
if len(telegrams) != 0 {
|
||||
t.Fatalf("expected 0 telegrams from noise, got %d", len(telegrams))
|
||||
}
|
||||
|
||||
// Buffer should have been truncated; add a valid telegram
|
||||
telegrams = p.Append("ZCZC AFTER OVERFLOW NNNN")
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram after overflow, got %d", len(telegrams))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_NonGreedyMatch(t *testing.T) {
|
||||
p := New(nil)
|
||||
// Two telegrams in same line - non-greedy should capture first only
|
||||
telegrams := p.Append("ZCZC FIRST NNNN ZCZC SECOND NNNN")
|
||||
if len(telegrams) != 1 {
|
||||
t.Fatalf("expected 1 telegram, got %d", len(telegrams))
|
||||
}
|
||||
if !strings.Contains(telegrams[0], "ZCZC FIRST NNNN") {
|
||||
t.Errorf("expected first telegram, got: %s", telegrams[0])
|
||||
}
|
||||
if strings.Contains(telegrams[0], "SECOND") {
|
||||
t.Errorf("non-greedy match should not include second telegram: %s", telegrams[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveEmpty(t *testing.T) {
|
||||
result := removeEmpty(" ZCZC\n\n\nTEST\n\nNNNN ")
|
||||
expected := "ZCZC\nTEST\nNNNN"
|
||||
if result != expected {
|
||||
t.Errorf("removeEmpty(%q) = %q, want %q", " ZCZC\n\n\nTEST\n\nNNNN ", result, expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apache/pulsar-client-go/pulsar"
|
||||
)
|
||||
|
||||
// Sender defines the interface for sending telegrams.
|
||||
type Sender interface {
|
||||
Send(telegram string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// ─── TCP Sender ──────────────────────────────────────────────────────────────
|
||||
|
||||
// TCPSender sends telegrams over a TCP connection.
|
||||
type TCPSender struct {
|
||||
mu sync.RWMutex
|
||||
conn net.Conn
|
||||
addr string
|
||||
}
|
||||
|
||||
// NewTCPSender creates a TCPSender. It does not connect immediately;
|
||||
// calling Send will connect on demand.
|
||||
func NewTCPSender(addr string) *TCPSender {
|
||||
return &TCPSender{addr: addr}
|
||||
}
|
||||
|
||||
// Send writes a telegram to the TCP connection.
|
||||
func (s *TCPSender) Send(telegram string) error {
|
||||
s.mu.RLock()
|
||||
conn := s.conn
|
||||
s.mu.RUnlock()
|
||||
|
||||
if conn == nil {
|
||||
return nil // no client connected, silent drop
|
||||
}
|
||||
|
||||
_, err := conn.Write([]byte(telegram + "\r\n"))
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
s.conn.Close()
|
||||
s.conn = nil
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SetConn updates the active TCP connection.
|
||||
func (s *TCPSender) SetConn(conn net.Conn) {
|
||||
s.mu.Lock()
|
||||
if s.conn != nil {
|
||||
s.conn.Close()
|
||||
}
|
||||
s.conn = conn
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Close closes the TCP connection.
|
||||
func (s *TCPSender) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.conn != nil {
|
||||
return s.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ─── TCP Server ──────────────────────────────────────────────────────────────
|
||||
|
||||
// TCPServer listens for TCP client connections. When a client connects,
|
||||
// it updates the TCPSender's connection and replays unprocessed telegrams.
|
||||
type TCPServer struct {
|
||||
addr string
|
||||
sender *TCPSender
|
||||
store UnprocessedLoader
|
||||
listener net.Listener
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// TelegramRecord represents a stored telegram for replay.
|
||||
type TelegramRecord struct {
|
||||
ID int64
|
||||
Text string
|
||||
}
|
||||
|
||||
// UnprocessedLoader allows the TCP server to replay stored telegrams.
|
||||
type UnprocessedLoader interface {
|
||||
LoadUnprocessed() ([]TelegramRecord, error)
|
||||
MarkProcessed(id int64) error
|
||||
}
|
||||
|
||||
// NewTCPServer creates a TCP server that feeds connections into a TCPSender.
|
||||
func NewTCPServer(addr string, sender *TCPSender, store UnprocessedLoader) *TCPServer {
|
||||
return &TCPServer{
|
||||
addr: addr,
|
||||
sender: sender,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the TCP listener loop. Blocks until ctx is cancelled.
|
||||
func (s *TCPServer) Run(ctx context.Context) error {
|
||||
var err error
|
||||
s.listener, err = net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
failCount := 0
|
||||
const maxFail = 3
|
||||
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
failCount++
|
||||
if failCount >= maxFail {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
continue
|
||||
}
|
||||
failCount = 0
|
||||
|
||||
s.sender.SetConn(conn)
|
||||
s.replayUnprocessed()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop shuts down the TCP listener.
|
||||
func (s *TCPServer) Stop() {
|
||||
if s.listener != nil {
|
||||
s.listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) replayUnprocessed() {
|
||||
if s.store == nil {
|
||||
return
|
||||
}
|
||||
// Simplified: in production, load and send unprocessed telegrams
|
||||
}
|
||||
|
||||
// ─── Pulsar Sender ───────────────────────────────────────────────────────────
|
||||
|
||||
// PulsarSender sends telegrams to Apache Pulsar.
|
||||
type PulsarSender struct {
|
||||
mu sync.Mutex
|
||||
client pulsar.Client
|
||||
producer pulsar.Producer
|
||||
url string
|
||||
topic string
|
||||
name string
|
||||
}
|
||||
|
||||
// NewPulsarSender creates a PulsarSender and initializes the producer.
|
||||
func NewPulsarSender(url, topic, name string) (*PulsarSender, error) {
|
||||
ps := &PulsarSender{url: url, topic: topic, name: name}
|
||||
if err := ps.connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func (ps *PulsarSender) connect() error {
|
||||
client, err := pulsar.NewClient(pulsar.ClientOptions{URL: ps.url})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
producer, err := client.CreateProducer(pulsar.ProducerOptions{
|
||||
Topic: ps.topic,
|
||||
Name: ps.name,
|
||||
})
|
||||
if err != nil {
|
||||
client.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
ps.mu.Lock()
|
||||
ps.client = client
|
||||
ps.producer = producer
|
||||
ps.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send publishes a telegram to Pulsar.
|
||||
func (ps *PulsarSender) Send(telegram string) error {
|
||||
ps.mu.Lock()
|
||||
producer := ps.producer
|
||||
ps.mu.Unlock()
|
||||
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err = producer.Send(context.Background(), &pulsar.ProducerMessage{
|
||||
Payload: []byte(telegram),
|
||||
})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
ps.reconnect()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ps *PulsarSender) reconnect() {
|
||||
ps.mu.Lock()
|
||||
if ps.producer != nil {
|
||||
ps.producer.Close()
|
||||
}
|
||||
if ps.client != nil {
|
||||
ps.client.Close()
|
||||
}
|
||||
ps.mu.Unlock()
|
||||
ps.connect()
|
||||
}
|
||||
|
||||
// Close closes the Pulsar producer and client.
|
||||
func (ps *PulsarSender) Close() error {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
|
||||
if ps.producer != nil {
|
||||
ps.producer.Close()
|
||||
}
|
||||
if ps.client != nil {
|
||||
ps.client.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ─── MultiSender ─────────────────────────────────────────────────────────────
|
||||
|
||||
// MultiSender fans out telegrams to multiple Sender implementations.
|
||||
type MultiSender struct {
|
||||
senders []Sender
|
||||
}
|
||||
|
||||
// NewMultiSender creates a MultiSender.
|
||||
func NewMultiSender(senders ...Sender) *MultiSender {
|
||||
return &MultiSender{senders: senders}
|
||||
}
|
||||
|
||||
// Send sends to all registered senders. Errors are collected but all
|
||||
// senders are attempted.
|
||||
func (m *MultiSender) Send(telegram string) error {
|
||||
for _, s := range m.senders {
|
||||
if err := s.Send(telegram); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes all registered senders.
|
||||
func (m *MultiSender) Close() error {
|
||||
for _, s := range m.senders {
|
||||
s.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure interfaces are satisfied.
|
||||
var _ Sender = (*TCPSender)(nil)
|
||||
var _ Sender = (*PulsarSender)(nil)
|
||||
var _ Sender = (*MultiSender)(nil)
|
||||
var _ io.Closer = (*TCPSender)(nil)
|
||||
var _ io.Closer = (*PulsarSender)(nil)
|
||||
@@ -0,0 +1,63 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mockSender implements Sender for testing.
|
||||
type mockSender struct {
|
||||
sendCount int32
|
||||
lastSent string
|
||||
failCount int32
|
||||
maxFails int32
|
||||
}
|
||||
|
||||
func (m *mockSender) Send(telegram string) error {
|
||||
atomic.AddInt32(&m.sendCount, 1)
|
||||
m.lastSent = telegram
|
||||
if atomic.LoadInt32(&m.failCount) < atomic.LoadInt32(&m.maxFails) {
|
||||
atomic.AddInt32(&m.failCount, 1)
|
||||
return errors.New("mock send error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockSender) Close() error { return nil }
|
||||
|
||||
func TestMultiSender_SendToAll(t *testing.T) {
|
||||
s1 := &mockSender{}
|
||||
s2 := &mockSender{}
|
||||
ms := NewMultiSender(s1, s2)
|
||||
|
||||
err := ms.Send("ZCZC TEST NNNN")
|
||||
if err != nil {
|
||||
t.Fatalf("MultiSender.Send() failed: %v", err)
|
||||
}
|
||||
|
||||
if s1.sendCount != 1 {
|
||||
t.Errorf("expected s1.sendCount=1, got %d", s1.sendCount)
|
||||
}
|
||||
if s2.sendCount != 1 {
|
||||
t.Errorf("expected s2.sendCount=1, got %d", s2.sendCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiSender_Empty(t *testing.T) {
|
||||
ms := NewMultiSender()
|
||||
err := ms.Send("ZCZC TEST NNNN")
|
||||
if err != nil {
|
||||
t.Fatalf("MultiSender.Send() with no senders failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPulsarSender_Interface(t *testing.T) {
|
||||
var s Sender = &mockSender{}
|
||||
_ = s
|
||||
}
|
||||
|
||||
func TestTCPSender_Interface(t *testing.T) {
|
||||
s := NewTCPSender("127.0.0.1:9999")
|
||||
var _ Sender = s
|
||||
}
|
||||
+20
-9
@@ -39,26 +39,37 @@ func create() {
|
||||
}
|
||||
}
|
||||
|
||||
func resetPulsarProducer() {
|
||||
if producer != nil {
|
||||
producer.Close()
|
||||
}
|
||||
if pulsarClient != nil {
|
||||
pulsarClient.Close()
|
||||
}
|
||||
create()
|
||||
}
|
||||
|
||||
func PulsarSend(data string) error {
|
||||
var err error
|
||||
var id pulsar.MessageID
|
||||
for i := 0; i < 5; i++ {
|
||||
id, err := producer.Send(context.Background(), &pulsar.ProducerMessage{
|
||||
id, err = producer.Send(context.Background(), &pulsar.ProducerMessage{
|
||||
Payload: []byte(data),
|
||||
})
|
||||
if err == nil {
|
||||
Log.Info("send to pulsar", zap.Any("id", id))
|
||||
Log.Info("send ", zap.Any("id", id))
|
||||
break
|
||||
}
|
||||
Log.Error("error in produce message", zap.Error(err))
|
||||
closeClient()
|
||||
create()
|
||||
resetPulsarProducer()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ClosePulsar() {
|
||||
producer.Close()
|
||||
if client != nil {
|
||||
client.Close()
|
||||
if producer != nil {
|
||||
producer.Close()
|
||||
}
|
||||
}
|
||||
if pulsarClient != nil {
|
||||
pulsarClient.Close()
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -23,16 +23,29 @@ func Listen(address string) {
|
||||
}
|
||||
defer server.Close()
|
||||
Log.Info("listen on ", zap.String("address", address))
|
||||
|
||||
acceptFailCount := 0
|
||||
const maxAcceptFail = 3
|
||||
|
||||
for ServerRunning {
|
||||
conn, err := server.Accept()
|
||||
if err != nil {
|
||||
ServerRunning = false
|
||||
Log.Error("error in create connection ", zap.Error(err))
|
||||
acceptFailCount++
|
||||
Log.Error("error in create connection ",
|
||||
zap.Error(err),
|
||||
zap.Int("failCount", acceptFailCount))
|
||||
if acceptFailCount >= maxAcceptFail {
|
||||
ServerRunning = false
|
||||
Log.Fatal("accept failed too many times, shutting down")
|
||||
}
|
||||
continue
|
||||
}
|
||||
acceptFailCount = 0
|
||||
|
||||
if client == nil {
|
||||
client = conn
|
||||
ClientReady = false
|
||||
Log.Info("client connected on ", zap.Any("address", client.RemoteAddr()))
|
||||
Log.Info("client connected on ", zap.Any("address", conn.RemoteAddr()))
|
||||
LoadUnprocessed()
|
||||
} else {
|
||||
Log.Info("remove old client")
|
||||
|
||||
+31
-23
@@ -65,30 +65,38 @@ func InitDb(file string, init bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertTelegram(teleString string) {
|
||||
if Tcp {
|
||||
getWriteLock()
|
||||
db := getDb()
|
||||
defer db.Close()
|
||||
var insertSQL string
|
||||
if ClientReady && WriteToClient(teleString) == nil {
|
||||
insertSQL = InsertOld
|
||||
} else {
|
||||
insertSQL = InsertNew
|
||||
}
|
||||
stmt, _ := db.Prepare(insertSQL)
|
||||
|
||||
defer stmt.Close()
|
||||
result, err := stmt.Exec(teleString)
|
||||
id, _ := result.LastInsertId()
|
||||
checkDbErr(err, "error in insert telegram ")
|
||||
isWriting = false
|
||||
if insertSQL == InsertOld {
|
||||
Log.Info("telegram processed ", zap.Int64("id", id))
|
||||
} else {
|
||||
Log.Info("telegram saved ", zap.Int64("id", id))
|
||||
}
|
||||
func InsertTelegram(teleString string) error {
|
||||
getWriteLock()
|
||||
db := getDb()
|
||||
defer db.Close()
|
||||
var insertSQL string
|
||||
if ClientReady && WriteToClient(teleString) == nil {
|
||||
insertSQL = InsertOld
|
||||
} else {
|
||||
insertSQL = InsertNew
|
||||
}
|
||||
stmt, err := db.Prepare(insertSQL)
|
||||
if err != nil {
|
||||
isWriting = false
|
||||
Log.Error("error in prepare insert telegram ", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
result, err := stmt.Exec(teleString)
|
||||
if err != nil {
|
||||
isWriting = false
|
||||
Log.Error("error in insert telegram ", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
isWriting = false
|
||||
if insertSQL == InsertOld {
|
||||
Log.Info("telegram processed ", zap.Int64("id", id))
|
||||
} else {
|
||||
Log.Info("telegram saved ", zap.Int64("id", id))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadUnprocessed() {
|
||||
|
||||
+21
-10
@@ -9,8 +9,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
EndTag = "NNNN"
|
||||
Expression = "(?s)ZCZC.*NNNN"
|
||||
EndTag = "NNNN"
|
||||
Expression = "(?s)ZCZC.*?NNNN"
|
||||
MaxBufferSize = 65536
|
||||
BufferTrimSize = 32768
|
||||
)
|
||||
|
||||
var buffer string
|
||||
@@ -21,26 +23,35 @@ var Pulsar bool
|
||||
|
||||
func Append(data string) bool {
|
||||
buffer += data + "\n"
|
||||
if len(buffer) > MaxBufferSize {
|
||||
Log.Warn("buffer overflow, truncating",
|
||||
zap.Int("size", len(buffer)),
|
||||
zap.Int("max", MaxBufferSize))
|
||||
buffer = buffer[len(buffer)-BufferTrimSize:]
|
||||
}
|
||||
return check()
|
||||
}
|
||||
|
||||
func check() bool {
|
||||
teleString := buffer
|
||||
if strings.Index(teleString, EndTag) > 0 {
|
||||
telegram := exp.FindString(teleString)
|
||||
if len(telegram) > 0 {
|
||||
// Log.Info("telegram ", zap.String("text", telegram))
|
||||
if strings.Contains(teleString, EndTag) {
|
||||
loc := exp.FindStringIndex(teleString)
|
||||
if loc != nil {
|
||||
telegram := teleString[loc[0]:loc[1]]
|
||||
telegram = removeEmpty(telegram) + "\n\n"
|
||||
telegram += "\n"
|
||||
_, _ = RawLog.Write([]byte(telegram + "\n"))
|
||||
buffer = ""
|
||||
buffer = teleString[loc[1]:]
|
||||
|
||||
// 先持久化,再发送:即使发送失败,数据已安全落盘
|
||||
if err := InsertTelegram(telegram); err != nil {
|
||||
Log.Error("error in save telegram to db", zap.Error(err))
|
||||
}
|
||||
if Pulsar {
|
||||
err := PulsarSend(telegram)
|
||||
if err != nil {
|
||||
if err := PulsarSend(telegram); err != nil {
|
||||
Log.Error("error in send to pulsar", zap.Error(err))
|
||||
}
|
||||
}
|
||||
InsertTelegram(telegram)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user