1
0

process.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. type lastLineWriter struct {
  45. mu sync.Mutex
  46. lastLine string
  47. }
  48. func (w *lastLineWriter) Write(p []byte) (int, error) {
  49. line := strings.TrimSpace(string(p))
  50. if line != "" {
  51. w.mu.Lock()
  52. w.lastLine = line
  53. w.mu.Unlock()
  54. }
  55. return len(p), nil
  56. }
  57. func (w *lastLineWriter) LastLine() string {
  58. w.mu.Lock()
  59. defer w.mu.Unlock()
  60. return w.lastLine
  61. }
  62. // Process wraps a single mtg process invocation for one mtproto inbound.
  63. type Process struct {
  64. cmd *exec.Cmd
  65. done chan struct{}
  66. configPath string
  67. logWriter *lastLineWriter
  68. exitErr error
  69. intentionalStop atomic.Bool
  70. }
  71. func newProcess(configPath string) *Process {
  72. return &Process{
  73. configPath: configPath,
  74. logWriter: &lastLineWriter{},
  75. }
  76. }
  77. // IsRunning reports whether the mtg process is currently running.
  78. func (p *Process) IsRunning() bool {
  79. if p.cmd == nil || p.cmd.Process == nil {
  80. return false
  81. }
  82. if p.done != nil {
  83. select {
  84. case <-p.done:
  85. return false
  86. default:
  87. }
  88. }
  89. if p.cmd.ProcessState == nil {
  90. return true
  91. }
  92. return false
  93. }
  94. // GetResult returns the last log line or the exit error from the mtg process.
  95. func (p *Process) GetResult() string {
  96. if line := p.logWriter.LastLine(); line != "" {
  97. return line
  98. }
  99. if p.exitErr != nil {
  100. return p.exitErr.Error()
  101. }
  102. return ""
  103. }
  104. // Start launches the mtg process against its generated config file.
  105. func (p *Process) Start() error {
  106. if p.IsRunning() {
  107. return errors.New("mtg is already running")
  108. }
  109. cmd := exec.Command(GetBinaryPath(), "run", p.configPath)
  110. cmd.Stdout = p.logWriter
  111. cmd.Stderr = p.logWriter
  112. p.cmd = cmd
  113. p.done = make(chan struct{})
  114. p.exitErr = nil
  115. p.intentionalStop.Store(false)
  116. if err := cmd.Start(); err != nil {
  117. close(p.done)
  118. p.cmd = nil
  119. return err
  120. }
  121. attachChildLifetime(cmd)
  122. go p.wait(cmd)
  123. return nil
  124. }
  125. func (p *Process) wait(cmd *exec.Cmd) {
  126. defer close(p.done)
  127. err := cmd.Wait()
  128. if err == nil || p.intentionalStop.Load() {
  129. return
  130. }
  131. if runtime.GOOS == "windows" {
  132. if strings.Contains(strings.ToLower(err.Error()), "exit status 1") {
  133. p.exitErr = err
  134. return
  135. }
  136. }
  137. logger.Error("mtproto: mtg process exited:", err)
  138. p.exitErr = err
  139. }
  140. // Stop terminates the running mtg process gracefully, falling back to a kill.
  141. func (p *Process) Stop() error {
  142. if !p.IsRunning() {
  143. return errors.New("mtg is not running")
  144. }
  145. p.intentionalStop.Store(true)
  146. if runtime.GOOS == "windows" {
  147. if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  148. return err
  149. }
  150. return p.waitForExit(forceStopTimeout)
  151. }
  152. if err := p.cmd.Process.Signal(syscall.SIGTERM); err != nil {
  153. if errors.Is(err, os.ErrProcessDone) {
  154. return p.waitForExit(forceStopTimeout)
  155. }
  156. return err
  157. }
  158. if err := p.waitForExit(gracefulStopTimeout); err == nil {
  159. return nil
  160. }
  161. logger.Warning("mtproto: mtg did not stop after SIGTERM, killing process")
  162. if err := p.cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
  163. return err
  164. }
  165. return p.waitForExit(forceStopTimeout)
  166. }
  167. func (p *Process) waitForExit(timeout time.Duration) error {
  168. if p.done == nil {
  169. return nil
  170. }
  171. timer := time.NewTimer(timeout)
  172. defer timer.Stop()
  173. select {
  174. case <-p.done:
  175. return nil
  176. case <-timer.C:
  177. return fmt.Errorf("timed out waiting for mtg process to stop after %s", timeout)
  178. }
  179. }