process.go 5.5 KB

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