1
0

process.go 5.6 KB

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