process.go 5.5 KB

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