process.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. // Package mtproto manages mtg-multi (github.com/mhsanaei/mtg-multi) sidecar
  2. // processes that serve MTProto FakeTLS proxies. Xray-core has no mtproto
  3. // protocol, so mtproto inbounds are run as standalone mtg processes — one
  4. // process per inbound, each serving every active client's secret through the
  5. // mtg-multi [secrets] section — entirely outside the Xray config and lifecycle.
  6. // A client edit is hot-applied via the fork's POST /reload endpoint so live
  7. // connections survive; the manager falls back to a restart on older binaries.
  8. package mtproto
  9. import (
  10. "context"
  11. "errors"
  12. "fmt"
  13. "os"
  14. "os/exec"
  15. "runtime"
  16. "strings"
  17. "sync"
  18. "sync/atomic"
  19. "syscall"
  20. "time"
  21. "github.com/mhsanaei/3x-ui/v3/internal/config"
  22. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  23. )
  24. // GetBinaryName returns the mtg binary filename for the current OS and arch,
  25. // matching the naming scheme used for the Xray binary. On Windows the ".exe"
  26. // extension is appended so a natural "mtg-windows-amd64.exe" is found.
  27. func GetBinaryName() string {
  28. name := fmt.Sprintf("mtg-%s-%s", runtime.GOOS, runtime.GOARCH)
  29. if runtime.GOOS == "windows" {
  30. name += ".exe"
  31. }
  32. return name
  33. }
  34. // GetBinaryPath returns the full path to the mtg binary, alongside the Xray binary.
  35. func GetBinaryPath() string {
  36. return config.GetBinFolderPath() + "/" + GetBinaryName()
  37. }
  38. func configDir() string {
  39. return config.GetBinFolderPath() + "/mtproto"
  40. }
  41. func configPathForID(id int) string {
  42. return fmt.Sprintf("%s/mtg-%d.toml", configDir(), id)
  43. }
  44. var (
  45. gracefulStopTimeout = 5 * time.Second
  46. forceStopTimeout = 2 * time.Second
  47. )
  48. // procLogWriter consumes the mtg child process's stdout/stderr. It splits the
  49. // stream into lines, forwards each one to the x-ui log — so mtg's own messages,
  50. // including why it cannot reach Telegram, become visible in the panel log viewer
  51. // and journald — and remembers the most recent line for GetResult.
  52. type procLogWriter struct {
  53. mu sync.Mutex
  54. label string
  55. buf string
  56. lastLine string
  57. }
  58. func (w *procLogWriter) Write(p []byte) (int, error) {
  59. w.mu.Lock()
  60. defer w.mu.Unlock()
  61. w.buf += string(p)
  62. for {
  63. i := strings.IndexByte(w.buf, '\n')
  64. if i < 0 {
  65. break
  66. }
  67. line := w.buf[:i]
  68. w.buf = w.buf[i+1:]
  69. w.emitLocked(line)
  70. }
  71. return len(p), nil
  72. }
  73. // Flush emits any buffered partial line; called once the process exits so a
  74. // final un-terminated error line is not lost.
  75. func (w *procLogWriter) Flush() {
  76. w.mu.Lock()
  77. defer w.mu.Unlock()
  78. if w.buf != "" {
  79. line := w.buf
  80. w.buf = ""
  81. w.emitLocked(line)
  82. }
  83. }
  84. func (w *procLogWriter) emitLocked(line string) {
  85. trimmed := strings.TrimSpace(strings.TrimRight(line, "\r"))
  86. if trimmed == "" {
  87. return
  88. }
  89. w.lastLine = trimmed
  90. logger.Infof("mtproto: mtg %s | %s", w.label, trimmed)
  91. }
  92. func (w *procLogWriter) LastLine() string {
  93. w.mu.Lock()
  94. defer w.mu.Unlock()
  95. return w.lastLine
  96. }
  97. // Process wraps a single mtg process invocation for one mtproto inbound.
  98. type Process struct {
  99. mu sync.RWMutex
  100. cmd *exec.Cmd
  101. done chan struct{}
  102. configPath string
  103. logWriter *procLogWriter
  104. exitErr error
  105. intentionalStop atomic.Bool
  106. }
  107. func newProcess(configPath, label string) *Process {
  108. return &Process{
  109. configPath: configPath,
  110. logWriter: &procLogWriter{label: label},
  111. }
  112. }
  113. // IsRunning reports whether the mtg process is currently running.
  114. func (p *Process) IsRunning() bool {
  115. p.mu.RLock()
  116. cmd, done := p.cmd, p.done
  117. p.mu.RUnlock()
  118. if cmd == nil || cmd.Process == nil {
  119. return false
  120. }
  121. if done != nil {
  122. select {
  123. case <-done:
  124. return false
  125. default:
  126. }
  127. }
  128. return true
  129. }
  130. // GetResult returns the last log line or the exit error from the mtg process.
  131. func (p *Process) GetResult() string {
  132. if line := p.logWriter.LastLine(); line != "" {
  133. return line
  134. }
  135. p.mu.RLock()
  136. exitErr := p.exitErr
  137. p.mu.RUnlock()
  138. if exitErr != nil {
  139. return exitErr.Error()
  140. }
  141. return ""
  142. }
  143. // Start launches the mtg process against its generated config file.
  144. func (p *Process) Start() error {
  145. if p.IsRunning() {
  146. return errors.New("mtg is already running")
  147. }
  148. cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "run", p.configPath)
  149. cmd.Stdout = p.logWriter
  150. cmd.Stderr = p.logWriter
  151. done := make(chan struct{})
  152. p.mu.Lock()
  153. p.cmd = cmd
  154. p.done = done
  155. p.exitErr = nil
  156. p.mu.Unlock()
  157. p.intentionalStop.Store(false)
  158. if err := cmd.Start(); err != nil {
  159. close(done)
  160. p.mu.Lock()
  161. p.cmd = nil
  162. p.mu.Unlock()
  163. return err
  164. }
  165. attachChildLifetime(cmd)
  166. go p.wait(cmd, done)
  167. return nil
  168. }
  169. func (p *Process) wait(cmd *exec.Cmd, done chan struct{}) {
  170. defer close(done)
  171. err := cmd.Wait()
  172. p.logWriter.Flush()
  173. if err == nil || p.intentionalStop.Load() {
  174. return
  175. }
  176. if runtime.GOOS == "windows" {
  177. if strings.Contains(strings.ToLower(err.Error()), "exit status 1") {
  178. p.setExitErr(err)
  179. return
  180. }
  181. }
  182. logger.Errorf("mtproto: mtg process exited: %v", err)
  183. p.setExitErr(err)
  184. }
  185. func (p *Process) setExitErr(err error) {
  186. p.mu.Lock()
  187. p.exitErr = err
  188. p.mu.Unlock()
  189. }
  190. // Stop terminates the running mtg process gracefully, falling back to a kill.
  191. func (p *Process) Stop() error {
  192. if !p.IsRunning() {
  193. return errors.New("mtg is not running")
  194. }
  195. p.intentionalStop.Store(true)
  196. p.mu.RLock()
  197. cmd, done := p.cmd, p.done
  198. p.mu.RUnlock()
  199. if cmd == nil || cmd.Process == nil {
  200. return errors.New("mtg is not running")
  201. }
  202. if runtime.GOOS == "windows" {
  203. if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  204. return err
  205. }
  206. return waitForExit(done, forceStopTimeout)
  207. }
  208. if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
  209. if errors.Is(err, os.ErrProcessDone) {
  210. return waitForExit(done, forceStopTimeout)
  211. }
  212. return err
  213. }
  214. if err := waitForExit(done, gracefulStopTimeout); err == nil {
  215. return nil
  216. }
  217. logger.Warning("mtproto: mtg did not stop after SIGTERM, killing process")
  218. if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  219. return err
  220. }
  221. return waitForExit(done, forceStopTimeout)
  222. }
  223. func waitForExit(done <-chan struct{}, timeout time.Duration) error {
  224. if done == nil {
  225. return nil
  226. }
  227. timer := time.NewTimer(timeout)
  228. defer timer.Stop()
  229. select {
  230. case <-done:
  231. return nil
  232. case <-timer.C:
  233. return fmt.Errorf("timed out waiting for mtg process to stop after %s", timeout)
  234. }
  235. }