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"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"it2000.com.cn/tele-recv/config"
|
||||||
"it2000.com.cn/tele-recv/utils"
|
"it2000.com.cn/tele-recv/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ModeName = "TELEGRAM_MODE"
|
const ModeName = "TELEGRAM_MODE"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfgFile string
|
cfgFile string
|
||||||
device string
|
|
||||||
baudrate int
|
|
||||||
lograw bool
|
|
||||||
|
|
||||||
dbFile string
|
|
||||||
dbInit bool
|
|
||||||
|
|
||||||
|
// Legacy global vars for backward compatibility with test command
|
||||||
|
device string
|
||||||
|
baudrate int
|
||||||
|
dbFile string
|
||||||
|
dbInit bool
|
||||||
socketAddress string
|
socketAddress string
|
||||||
|
pulsarUrl string
|
||||||
|
topic string
|
||||||
|
name string
|
||||||
|
tcp bool
|
||||||
|
pulsar bool
|
||||||
|
lograw bool
|
||||||
|
|
||||||
pulsarUrl string
|
Mode string
|
||||||
topic string
|
|
||||||
name string
|
|
||||||
|
|
||||||
tcp bool
|
|
||||||
pulsar bool
|
|
||||||
|
|
||||||
// rootCmd represents the base command when called without any subcommands
|
// rootCmd represents the base command when called without any subcommands
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "tele-recv",
|
Use: "tele-recv",
|
||||||
Short: "telegram receiver",
|
Short: "telegram receiver",
|
||||||
Long: `A serial port telegram receiver.
|
Long: `A serial port telegram receiver.
|
||||||
Read the telegram from `,
|
Read the telegram from serial port`,
|
||||||
// Uncomment the following line if your bare application
|
|
||||||
// has an action associated with it:
|
|
||||||
// Run: func(cmd *cobra.Command, args []string) { },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mode = os.Getenv(ModeName)
|
appCfg *config.Config
|
||||||
|
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,55 +73,36 @@ func Execute() {
|
|||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
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)")
|
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")
|
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
|
||||||
initLog()
|
initLog()
|
||||||
defer logger.Sync()
|
|
||||||
// l = logger.Sugar()
|
|
||||||
utils.Log = logger
|
utils.Log = logger
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initConfig reads in config file and ENV variables if set.
|
// initConfig reads in config file and ENV variables if set.
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
if cfgFile != "" {
|
cfg, err := config.Load(cfgFile)
|
||||||
// Use config file from the flag.
|
if err != nil {
|
||||||
viper.SetConfigFile(cfgFile)
|
logger.Warn("config load error, using defaults", zap.Error(err))
|
||||||
} else {
|
return
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
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() {
|
func initLog() {
|
||||||
@@ -163,5 +140,4 @@ func initLog() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger = zap.New(core)
|
logger = zap.New(core)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-80
@@ -16,15 +16,10 @@ limitations under the License.
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"it2000.com.cn/tele-recv/utils"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"it2000.com.cn/tele-recv/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
// startCmd represents the start command
|
// startCmd represents the start command
|
||||||
@@ -33,85 +28,24 @@ var startCmd = &cobra.Command{
|
|||||||
Short: "start telegram receive service",
|
Short: "start telegram receive service",
|
||||||
Long: `open serial port
|
Long: `open serial port
|
||||||
start a tcp server for processing`,
|
start a tcp server for processing`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
start()
|
return start()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(startCmd)
|
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() {
|
func start() error {
|
||||||
// Go signal notification works by sending `os.Signal`
|
if appCfg == nil {
|
||||||
// values on a channel. We'll create a channel to
|
logger.Fatal("configuration not loaded")
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
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() {
|
func init() {
|
||||||
rootCmd.AddCommand(testCmd)
|
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
|
module it2000.com.cn/tele-recv
|
||||||
|
|
||||||
go 1.15
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/apache/pulsar-client-go v0.3.0
|
github.com/apache/pulsar-client-go v0.3.0
|
||||||
github.com/argandas/serial v0.0.0-20160316175758-889a5ad85462
|
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/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/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/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/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/afero v1.5.1 // indirect
|
||||||
github.com/spf13/cast v1.3.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/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/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/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/sys v0.0.0-20201214210602-f9fddec55a1e // indirect
|
||||||
golang.org/x/text v0.3.4 // 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/ini.v1 v1.62.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.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/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/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/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 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 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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/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/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/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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
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 h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
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.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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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=
|
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/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 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-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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 {
|
func PulsarSend(data string) error {
|
||||||
var err error
|
var err error
|
||||||
|
var id pulsar.MessageID
|
||||||
for i := 0; i < 5; i++ {
|
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),
|
Payload: []byte(data),
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
Log.Info("send to pulsar", zap.Any("id", id))
|
Log.Info("send ", zap.Any("id", id))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
Log.Error("error in produce message", zap.Error(err))
|
resetPulsarProducer()
|
||||||
closeClient()
|
|
||||||
create()
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClosePulsar() {
|
func ClosePulsar() {
|
||||||
producer.Close()
|
if producer != nil {
|
||||||
if client != nil {
|
producer.Close()
|
||||||
client.Close()
|
|
||||||
}
|
}
|
||||||
}
|
if pulsarClient != nil {
|
||||||
|
pulsarClient.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-3
@@ -23,16 +23,29 @@ func Listen(address string) {
|
|||||||
}
|
}
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
Log.Info("listen on ", zap.String("address", address))
|
Log.Info("listen on ", zap.String("address", address))
|
||||||
|
|
||||||
|
acceptFailCount := 0
|
||||||
|
const maxAcceptFail = 3
|
||||||
|
|
||||||
for ServerRunning {
|
for ServerRunning {
|
||||||
conn, err := server.Accept()
|
conn, err := server.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ServerRunning = false
|
acceptFailCount++
|
||||||
Log.Error("error in create connection ", zap.Error(err))
|
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 {
|
if client == nil {
|
||||||
client = conn
|
client = conn
|
||||||
ClientReady = false
|
ClientReady = false
|
||||||
Log.Info("client connected on ", zap.Any("address", client.RemoteAddr()))
|
Log.Info("client connected on ", zap.Any("address", conn.RemoteAddr()))
|
||||||
LoadUnprocessed()
|
LoadUnprocessed()
|
||||||
} else {
|
} else {
|
||||||
Log.Info("remove old client")
|
Log.Info("remove old client")
|
||||||
|
|||||||
+31
-23
@@ -65,30 +65,38 @@ func InitDb(file string, init bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InsertTelegram(teleString string) {
|
func InsertTelegram(teleString string) error {
|
||||||
if Tcp {
|
getWriteLock()
|
||||||
getWriteLock()
|
db := getDb()
|
||||||
db := getDb()
|
defer db.Close()
|
||||||
defer db.Close()
|
var insertSQL string
|
||||||
var insertSQL string
|
if ClientReady && WriteToClient(teleString) == nil {
|
||||||
if ClientReady && WriteToClient(teleString) == nil {
|
insertSQL = InsertOld
|
||||||
insertSQL = InsertOld
|
} else {
|
||||||
} else {
|
insertSQL = InsertNew
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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() {
|
func LoadUnprocessed() {
|
||||||
|
|||||||
+21
-10
@@ -9,8 +9,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EndTag = "NNNN"
|
EndTag = "NNNN"
|
||||||
Expression = "(?s)ZCZC.*NNNN"
|
Expression = "(?s)ZCZC.*?NNNN"
|
||||||
|
MaxBufferSize = 65536
|
||||||
|
BufferTrimSize = 32768
|
||||||
)
|
)
|
||||||
|
|
||||||
var buffer string
|
var buffer string
|
||||||
@@ -21,26 +23,35 @@ var Pulsar bool
|
|||||||
|
|
||||||
func Append(data string) bool {
|
func Append(data string) bool {
|
||||||
buffer += data + "\n"
|
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()
|
return check()
|
||||||
}
|
}
|
||||||
|
|
||||||
func check() bool {
|
func check() bool {
|
||||||
teleString := buffer
|
teleString := buffer
|
||||||
if strings.Index(teleString, EndTag) > 0 {
|
if strings.Contains(teleString, EndTag) {
|
||||||
telegram := exp.FindString(teleString)
|
loc := exp.FindStringIndex(teleString)
|
||||||
if len(telegram) > 0 {
|
if loc != nil {
|
||||||
// Log.Info("telegram ", zap.String("text", telegram))
|
telegram := teleString[loc[0]:loc[1]]
|
||||||
telegram = removeEmpty(telegram) + "\n\n"
|
telegram = removeEmpty(telegram) + "\n\n"
|
||||||
telegram += "\n"
|
telegram += "\n"
|
||||||
_, _ = RawLog.Write([]byte(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 {
|
if Pulsar {
|
||||||
err := PulsarSend(telegram)
|
if err := PulsarSend(telegram); err != nil {
|
||||||
if err != nil {
|
|
||||||
Log.Error("error in send to pulsar", zap.Error(err))
|
Log.Error("error in send to pulsar", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InsertTelegram(telegram)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user