process.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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. cmd *exec.Cmd
  100. done chan struct{}
  101. configPath string
  102. logWriter *procLogWriter
  103. exitErr error
  104. intentionalStop atomic.Bool
  105. }
  106. func newProcess(configPath, label string) *Process {
  107. return &Process{
  108. configPath: configPath,
  109. logWriter: &procLogWriter{label: label},
  110. }
  111. }
  112. // IsRunning reports whether the mtg process is currently running.
  113. func (p *Process) IsRunning() bool {
  114. if p.cmd == nil || p.cmd.Process == nil {
  115. return false
  116. }
  117. if p.done != nil {
  118. select {
  119. case <-p.done:
  120. return false
  121. default:
  122. }
  123. }
  124. if p.cmd.ProcessState == nil {
  125. return true
  126. }
  127. return false
  128. }
  129. // GetResult returns the last log line or the exit error from the mtg process.
  130. func (p *Process) GetResult() string {
  131. if line := p.logWriter.LastLine(); line != "" {
  132. return line
  133. }
  134. if p.exitErr != nil {
  135. return p.exitErr.Error()
  136. }
  137. return ""
  138. }
  139. // Start launches the mtg process against its generated config file.
  140. func (p *Process) Start() error {
  141. if p.IsRunning() {
  142. return errors.New("mtg is already running")
  143. }
  144. cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "run", p.configPath)
  145. cmd.Stdout = p.logWriter
  146. cmd.Stderr = p.logWriter
  147. p.cmd = cmd
  148. p.done = make(chan struct{})
  149. p.exitErr = nil
  150. p.intentionalStop.Store(false)
  151. if err := cmd.Start(); err != nil {
  152. close(p.done)
  153. p.cmd = nil
  154. return err
  155. }
  156. attachChildLifetime(cmd)
  157. go p.wait(cmd)
  158. return nil
  159. }
  160. func (p *Process) wait(cmd *exec.Cmd) {
  161. defer close(p.done)
  162. err := cmd.Wait()
  163. p.logWriter.Flush()
  164. if err == nil || p.intentionalStop.Load() {
  165. return
  166. }
  167. if runtime.GOOS == "windows" {
  168. if strings.Contains(strings.ToLower(err.Error()), "exit status 1") {
  169. p.exitErr = err
  170. return
  171. }
  172. }
  173. logger.Errorf("mtproto: mtg process exited: %v", err)
  174. p.exitErr = err
  175. }
  176. // Stop terminates the running mtg process gracefully, falling back to a kill.
  177. func (p *Process) Stop() error {
  178. if !p.IsRunning() {
  179. return errors.New("mtg is not running")
  180. }
  181. p.intentionalStop.Store(true)
  182. if runtime.GOOS == "windows" {
  183. if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  184. return err
  185. }
  186. return p.waitForExit(forceStopTimeout)
  187. }
  188. if err := p.cmd.Process.Signal(syscall.SIGTERM); err != nil {
  189. if errors.Is(err, os.ErrProcessDone) {
  190. return p.waitForExit(forceStopTimeout)
  191. }
  192. return err
  193. }
  194. if err := p.waitForExit(gracefulStopTimeout); err == nil {
  195. return nil
  196. }
  197. logger.Warning("mtproto: mtg did not stop after SIGTERM, killing process")
  198. if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  199. return err
  200. }
  201. return p.waitForExit(forceStopTimeout)
  202. }
  203. func (p *Process) waitForExit(timeout time.Duration) error {
  204. if p.done == nil {
  205. return nil
  206. }
  207. timer := time.NewTimer(timeout)
  208. defer timer.Stop()
  209. select {
  210. case <-p.done:
  211. return nil
  212. case <-timer.C:
  213. return fmt.Errorf("timed out waiting for mtg process to stop after %s", timeout)
  214. }
  215. }