From 12ae6394fedddefd83e4b33baaf59947fd3c61de Mon Sep 17 00:00:00 2001 From: vladon Date: Fri, 26 Dec 2025 00:37:27 +0300 Subject: [PATCH 1/3] fix: improve Telegram bot handling for concurrent starts and graceful shutdown - Added logic to stop any existing long-polling loop when Start is called again. - Introduced a mutex to manage access to shared state variables, ensuring thread safety. - Updated the OnReceive method to prevent multiple concurrent executions. - Enhanced Stop method to ensure proper cleanup of resources and state management. --- web/service/tgbot.go | 96 +++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 06c51faa..3a98dcb4 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -174,6 +174,10 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { return err } + // If Start is called again (e.g. during reload), ensure any previous long-polling + // loop is stopped before creating a new bot / receiver. + StopBot() + // Initialize hash storage to store callback queries hashStorage = global.NewHashStorage(20 * time.Minute) @@ -207,6 +211,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { return err } + parsedAdminIds := make([]int64, 0) // Parse admin IDs from comma-separated string if tgBotID != "" { for _, adminID := range strings.Split(tgBotID, ",") { @@ -215,9 +220,12 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) return err } - adminIds = append(adminIds, int64(id)) + parsedAdminIds = append(parsedAdminIds, int64(id)) } } + tgBotMutex.Lock() + adminIds = parsedAdminIds + tgBotMutex.Unlock() // Get Telegram bot proxy URL tgBotProxy, err := t.settingService.GetTgBotProxy() @@ -252,10 +260,12 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { } // Start receiving Telegram bot messages - if !isRunning { + tgBotMutex.Lock() + alreadyRunning := isRunning || botCancel != nil + tgBotMutex.Unlock() + if !alreadyRunning { logger.Info("Telegram bot receiver started") go t.OnReceive() - isRunning = true } return nil @@ -300,6 +310,8 @@ func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*tel // IsRunning checks if the Telegram bot is currently running. func (t *Tgbot) IsRunning() bool { + tgBotMutex.Lock() + defer tgBotMutex.Unlock() return isRunning } @@ -317,34 +329,34 @@ func (t *Tgbot) SetHostname() { // Stop safely stops the Telegram bot's Long Polling operation. // This method now calls the global StopBot function and cleans up other resources. func (t *Tgbot) Stop() { - // Call the global StopBot function to gracefully shut down Long Polling StopBot() - - // Stop the bot handler (in case the goroutine hasn't exited yet) - if botHandler != nil { - botHandler.Stop() - } logger.Info("Stop Telegram receiver ...") - isRunning = false + tgBotMutex.Lock() adminIds = nil + tgBotMutex.Unlock() } // StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context. // This is the global function called from main.go's signal handler and t.Stop(). func StopBot() { + // Don't hold the mutex while cancelling/waiting. tgBotMutex.Lock() - defer tgBotMutex.Unlock() + cancel := botCancel + botCancel = nil + handler := botHandler + botHandler = nil + isRunning = false + tgBotMutex.Unlock() - if botCancel != nil { + if handler != nil { + handler.Stop() + } + + if cancel != nil { logger.Info("Sending cancellation signal to Telegram bot...") - - // Calling botCancel() cancels the context passed to UpdatesViaLongPolling, - // which stops the Long Polling operation and closes the updates channel, - // allowing the th.Start() goroutine to exit cleanly. - botCancel() - - botCancel = nil - // Giving the goroutine a small delay to exit cleanly. + // Cancels the context passed to UpdatesViaLongPolling; this closes updates channel + // and lets botHandler.Start() exit cleanly. + cancel() botWG.Wait() logger.Info("Telegram bot successfully stopped.") } @@ -379,36 +391,38 @@ func (t *Tgbot) OnReceive() { params := telego.GetUpdatesParams{ Timeout: 30, // Increased timeout to reduce API calls } - // --- GRACEFUL SHUTDOWN FIX: Context creation --- + // Strict singleton: never start a second long-polling loop. tgBotMutex.Lock() - - // Create a context with cancellation and store the cancel function. - var ctx context.Context - - // Check if botCancel is already set (to prevent race condition overwrite and goroutine leak) - if botCancel == nil { - ctx, botCancel = context.WithCancel(context.Background()) - } else { - // If botCancel is already set, use a non-cancellable context for this redundant call. - // This prevents overwriting the active botCancel and causing a goroutine leak from the previous call. - logger.Warning("TgBot OnReceive called concurrently. Using background context for redundant call.") - ctx = context.Background() // <<< ИЗМЕНЕНИЕ + if botCancel != nil || isRunning { + tgBotMutex.Unlock() + logger.Warning("TgBot OnReceive called while already running; ignoring.") + return } + ctx, cancel := context.WithCancel(context.Background()) + botCancel = cancel + isRunning = true + // Add to WaitGroup before releasing the lock so StopBot() can't return + // before this receiver goroutine is accounted for. + botWG.Add(1) tgBotMutex.Unlock() // Get updates channel using the context. updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms) - botWG.Go(func() { + go func() { + defer botWG.Done() + h, _ := th.NewBotHandler(bot, updates) + tgBotMutex.Lock() + botHandler = h + tgBotMutex.Unlock() - botHandler, _ = th.NewBotHandler(bot, updates) - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + h.HandleMessage(func(ctx *th.Context, message telego.Message) error { delete(userStates, message.Chat.ID) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) return nil }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + h.HandleMessage(func(ctx *th.Context, message telego.Message) error { // Use goroutine with worker pool for concurrent command processing go func() { messageWorkerPool <- struct{}{} // Acquire worker @@ -420,7 +434,7 @@ func (t *Tgbot) OnReceive() { return nil }, th.AnyCommand()) - botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { + h.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { // Use goroutine with worker pool for concurrent callback processing go func() { messageWorkerPool <- struct{}{} // Acquire worker @@ -432,7 +446,7 @@ func (t *Tgbot) OnReceive() { return nil }, th.AnyCallbackQueryWithMessage()) - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + h.HandleMessage(func(ctx *th.Context, message telego.Message) error { if userState, exists := userStates[message.Chat.ID]; exists { switch userState { case "awaiting_id": @@ -578,8 +592,8 @@ func (t *Tgbot) OnReceive() { return nil }, th.AnyMessage()) - botHandler.Start() - }) + h.Start() + }() } // answerCommand processes incoming command messages from Telegram users. From 74267cd320ac4d6d8869755e932aa6ff012a6d9a Mon Sep 17 00:00:00 2001 From: vladon Date: Fri, 26 Dec 2025 00:37:55 +0300 Subject: [PATCH 2/3] fix: enhance Telegram bot's long-polling management - Improved handling of concurrent starts by stopping existing long-polling loops. - Implemented mutex for thread-safe access to shared state variables. - Updated OnReceive method to prevent multiple executions. - Enhanced Stop method for better resource cleanup and state management. --- AGENTS.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ web/AGENTS.md | 23 +++++++++++++++++++++ xray/AGENTS.md | 19 ++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 AGENTS.md create mode 100644 web/AGENTS.md create mode 100644 xray/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..add09af6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# AGENTS.md (3x-ui) + +This repo is **primarily Go**: a Gin-based web panel that manages an **Xray-core** process and stores state in a **SQLite** DB. + +## Quick commands (repo root) + +- **Run (dev, no root needed)**: + - Ensure writable paths: + - `mkdir -p ./x-ui ./bin ./log` + - `export XUI_DB_FOLDER="$(pwd)/x-ui"` + - `export XUI_BIN_FOLDER="$(pwd)/bin"` + - `export XUI_LOG_FOLDER="$(pwd)/log"` + - `export XUI_DEBUG=true` (loads templates/assets from disk; see `web/AGENTS.md`) + - Start: `go run .` + - Panel defaults (fresh DB): `http://localhost:2053/` with **admin/admin** + +- **Build**: `go build -ldflags "-w -s" -o build/x-ui main.go` +- **Format**: `gofmt -w .` +- **Tests**: `go test ./...` +- **Basic sanity**: `go vet ./...` + +## Docker + +- **Compose**: `docker compose up --build` + - Uses `network_mode: host` and mounts: + - `./db/` → `/etc/x-ui/` (SQLite DB lives at `/etc/x-ui/x-ui.db`) + - `./cert/` → `/root/cert/` + +## Layout / where things live + +- **Entry point**: `main.go` (starts the web server + subscription server; handles signals) +- **Config**: `config/` (env-driven defaults; DB path, bin path, log folder) +- **DB (SQLite via GORM)**: `database/` (+ `database/model/`) +- **Web panel**: `web/` (Gin controllers, templates, embedded assets, i18n) +- **Subscription server**: `sub/` +- **Xray process management**: `xray/` (binary path naming, config/log paths, process wrapper) +- **Operational scripts**: `install.sh`, `update.sh`, `x-ui.sh` (production/admin tooling; be cautious editing) + +## Important environment variables + +- **`XUI_DEBUG=true`**: enables dev behavior (Gin debug + loads `web/html` and `web/assets` from disk). +- **`XUI_DB_FOLDER`**: DB directory (default: `/etc/x-ui` on non-Windows). DB file is `/x-ui.db`. +- **`XUI_BIN_FOLDER`**: where Xray binary + `config.json` + `geo*.dat` live (default: `bin`). +- **`XUI_LOG_FOLDER`**: log directory (default: `/var/log` on non-Windows). +- **`XUI_LOG_LEVEL`**: `debug|info|notice|warning|error`. + +## Agent workflow guidelines + +- **Prefer small, surgical changes**: this is a production-oriented project (panel + system scripts). +- **Don’t run** `install.sh` / `update.sh` in dev automation: they expect **root** and mutate the system. +- **When touching templates/assets**: ensure it works in both **debug** (disk) and **production** (embedded) modes. +- **Security**: treat any change in `web/controller`, `web/service`, and shell scripts as security-sensitive. + + diff --git a/web/AGENTS.md b/web/AGENTS.md new file mode 100644 index 00000000..d3c36bc9 --- /dev/null +++ b/web/AGENTS.md @@ -0,0 +1,23 @@ +# AGENTS.md (web/) + +`web/` is the **Go web panel** (Gin) and includes **embedded** templates/assets for production. + +## Templates & assets + +- **Templates**: `web/html/**` (server-rendered HTML templates containing Vue/Ant Design UI markup). +- **Static assets**: `web/assets/**` (vendored JS/CSS/libs). +- **Embedding**: + - Production uses `//go:embed` for `assets`, `html/*`, and `translation/*` (see `web/web.go`). + - Dev mode (`XUI_DEBUG=true`) loads templates from disk (`web/html`) and serves assets from disk (`web/assets`). + +## i18n / translations + +- Translation files live in `web/translation/*.toml` and are embedded. +- When adding UI strings, update the relevant TOML(s) and keep keys consistent across languages. + +## Common dev pitfalls + +- Run from repo root when `XUI_DEBUG=true` so `web/html` and `web/assets` resolve correctly. +- Some functionality depends on an Xray binary in `XUI_BIN_FOLDER` (default `bin/`); the panel can run without Xray but Xray-related features will fail until it’s available. + + diff --git a/xray/AGENTS.md b/xray/AGENTS.md new file mode 100644 index 00000000..92c02721 --- /dev/null +++ b/xray/AGENTS.md @@ -0,0 +1,19 @@ +# AGENTS.md (xray/) + +`xray/` wraps **Xray-core process management** (start/stop/restart, API traffic reads) and defines where runtime files live. + +## Runtime file paths (via `config/`) + +- **Binary**: `XUI_BIN_FOLDER/xray--` + - Example on Linux amd64: `bin/xray-linux-amd64` +- **Config**: `XUI_BIN_FOLDER/config.json` +- **Geo files**: `XUI_BIN_FOLDER/{geoip.dat,geosite.dat,...}` +- **Logs**: `XUI_LOG_FOLDER/*` (default `/var/log` on non-Windows) + +## Notes for changes + +- Keep OS/arch naming consistent with `GetBinaryName()` (`xray--`). +- The web panel may attempt to restart Xray periodically; if the binary is missing, Xray-related operations will fail but the panel can still run. +- Be careful with process/exec changes: they are security- and stability-sensitive. + + From 95ab90c2bd621b10fb128bdf11e8990229fdbc50 Mon Sep 17 00:00:00 2001 From: vladon Date: Fri, 26 Dec 2025 00:51:21 +0300 Subject: [PATCH 3/3] . --- AGENTS.md | 54 -------------------------------------------------- web/AGENTS.md | 23 --------------------- xray/AGENTS.md | 19 ------------------ 3 files changed, 96 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 web/AGENTS.md delete mode 100644 xray/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index add09af6..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,54 +0,0 @@ -# AGENTS.md (3x-ui) - -This repo is **primarily Go**: a Gin-based web panel that manages an **Xray-core** process and stores state in a **SQLite** DB. - -## Quick commands (repo root) - -- **Run (dev, no root needed)**: - - Ensure writable paths: - - `mkdir -p ./x-ui ./bin ./log` - - `export XUI_DB_FOLDER="$(pwd)/x-ui"` - - `export XUI_BIN_FOLDER="$(pwd)/bin"` - - `export XUI_LOG_FOLDER="$(pwd)/log"` - - `export XUI_DEBUG=true` (loads templates/assets from disk; see `web/AGENTS.md`) - - Start: `go run .` - - Panel defaults (fresh DB): `http://localhost:2053/` with **admin/admin** - -- **Build**: `go build -ldflags "-w -s" -o build/x-ui main.go` -- **Format**: `gofmt -w .` -- **Tests**: `go test ./...` -- **Basic sanity**: `go vet ./...` - -## Docker - -- **Compose**: `docker compose up --build` - - Uses `network_mode: host` and mounts: - - `./db/` → `/etc/x-ui/` (SQLite DB lives at `/etc/x-ui/x-ui.db`) - - `./cert/` → `/root/cert/` - -## Layout / where things live - -- **Entry point**: `main.go` (starts the web server + subscription server; handles signals) -- **Config**: `config/` (env-driven defaults; DB path, bin path, log folder) -- **DB (SQLite via GORM)**: `database/` (+ `database/model/`) -- **Web panel**: `web/` (Gin controllers, templates, embedded assets, i18n) -- **Subscription server**: `sub/` -- **Xray process management**: `xray/` (binary path naming, config/log paths, process wrapper) -- **Operational scripts**: `install.sh`, `update.sh`, `x-ui.sh` (production/admin tooling; be cautious editing) - -## Important environment variables - -- **`XUI_DEBUG=true`**: enables dev behavior (Gin debug + loads `web/html` and `web/assets` from disk). -- **`XUI_DB_FOLDER`**: DB directory (default: `/etc/x-ui` on non-Windows). DB file is `/x-ui.db`. -- **`XUI_BIN_FOLDER`**: where Xray binary + `config.json` + `geo*.dat` live (default: `bin`). -- **`XUI_LOG_FOLDER`**: log directory (default: `/var/log` on non-Windows). -- **`XUI_LOG_LEVEL`**: `debug|info|notice|warning|error`. - -## Agent workflow guidelines - -- **Prefer small, surgical changes**: this is a production-oriented project (panel + system scripts). -- **Don’t run** `install.sh` / `update.sh` in dev automation: they expect **root** and mutate the system. -- **When touching templates/assets**: ensure it works in both **debug** (disk) and **production** (embedded) modes. -- **Security**: treat any change in `web/controller`, `web/service`, and shell scripts as security-sensitive. - - diff --git a/web/AGENTS.md b/web/AGENTS.md deleted file mode 100644 index d3c36bc9..00000000 --- a/web/AGENTS.md +++ /dev/null @@ -1,23 +0,0 @@ -# AGENTS.md (web/) - -`web/` is the **Go web panel** (Gin) and includes **embedded** templates/assets for production. - -## Templates & assets - -- **Templates**: `web/html/**` (server-rendered HTML templates containing Vue/Ant Design UI markup). -- **Static assets**: `web/assets/**` (vendored JS/CSS/libs). -- **Embedding**: - - Production uses `//go:embed` for `assets`, `html/*`, and `translation/*` (see `web/web.go`). - - Dev mode (`XUI_DEBUG=true`) loads templates from disk (`web/html`) and serves assets from disk (`web/assets`). - -## i18n / translations - -- Translation files live in `web/translation/*.toml` and are embedded. -- When adding UI strings, update the relevant TOML(s) and keep keys consistent across languages. - -## Common dev pitfalls - -- Run from repo root when `XUI_DEBUG=true` so `web/html` and `web/assets` resolve correctly. -- Some functionality depends on an Xray binary in `XUI_BIN_FOLDER` (default `bin/`); the panel can run without Xray but Xray-related features will fail until it’s available. - - diff --git a/xray/AGENTS.md b/xray/AGENTS.md deleted file mode 100644 index 92c02721..00000000 --- a/xray/AGENTS.md +++ /dev/null @@ -1,19 +0,0 @@ -# AGENTS.md (xray/) - -`xray/` wraps **Xray-core process management** (start/stop/restart, API traffic reads) and defines where runtime files live. - -## Runtime file paths (via `config/`) - -- **Binary**: `XUI_BIN_FOLDER/xray--` - - Example on Linux amd64: `bin/xray-linux-amd64` -- **Config**: `XUI_BIN_FOLDER/config.json` -- **Geo files**: `XUI_BIN_FOLDER/{geoip.dat,geosite.dat,...}` -- **Logs**: `XUI_LOG_FOLDER/*` (default `/var/log` on non-Windows) - -## Notes for changes - -- Keep OS/arch naming consistent with `GetBinaryName()` (`xray--`). -- The web panel may attempt to restart Xray periodically; if the binary is missing, Xray-related operations will fail but the panel can still run. -- Be careful with process/exec changes: they are security- and stability-sensitive. - -