@@ -25,6 +25,13 @@ type hgHandler struct {
2525 once sync.Once
2626 hgPath string
2727 hgPathErr error
28+
29+ mu sync.Mutex
30+ wg sync.WaitGroup
31+ ctx context.Context
32+ cancel func ()
33+ cmds []* exec.Cmd
34+ url map [string ]* url.URL
2835}
2936
3037func (h * hgHandler ) Available () bool {
@@ -34,6 +41,30 @@ func (h *hgHandler) Available() bool {
3441 return h .hgPathErr == nil
3542}
3643
44+ func (h * hgHandler ) Close () error {
45+ h .mu .Lock ()
46+ defer h .mu .Unlock ()
47+
48+ if h .cancel == nil {
49+ return nil
50+ }
51+
52+ h .cancel ()
53+ for _ , cmd := range h .cmds {
54+ h .wg .Add (1 )
55+ go func () {
56+ cmd .Wait ()
57+ h .wg .Done ()
58+ }()
59+ }
60+ h .wg .Wait ()
61+ h .url = nil
62+ h .cmds = nil
63+ h .ctx = nil
64+ h .cancel = nil
65+ return nil
66+ }
67+
3768func (h * hgHandler ) Handler (dir string , env []string , logger * log.Logger ) (http.Handler , error ) {
3869 if ! h .Available () {
3970 return nil , ServerNotInstalledError {name : "hg" }
@@ -50,10 +81,25 @@ func (h *hgHandler) Handler(dir string, env []string, logger *log.Logger) (http.
5081 // if "hg" works at all then "hg serve" works too, and we'll execute that as
5182 // a subprocess, using a reverse proxy to forward the request and response.
5283
53- ctx , cancel := context .WithCancel (req .Context ())
54- defer cancel ()
84+ h .mu .Lock ()
85+
86+ if h .ctx == nil {
87+ h .ctx , h .cancel = context .WithCancel (context .Background ())
88+ }
5589
56- cmd := exec .CommandContext (ctx , h .hgPath , "serve" , "--port" , "0" , "--address" , "localhost" , "--accesslog" , os .DevNull , "--name" , "vcweb" , "--print-url" )
90+ // Cache the hg server subprocess globally, because hg is too slow
91+ // to start a new one for each request. There are under a dozen different
92+ // repos we serve, so leaving a dozen processes around is not a big deal.
93+ u := h .url [dir ]
94+ if u != nil {
95+ h .mu .Unlock ()
96+ logger .Printf ("proxying hg request to %s" , u )
97+ httputil .NewSingleHostReverseProxy (u ).ServeHTTP (w , req )
98+ return
99+ }
100+
101+ logger .Printf ("starting hg serve for %s" , dir )
102+ cmd := exec .CommandContext (h .ctx , h .hgPath , "serve" , "--port" , "0" , "--address" , "localhost" , "--accesslog" , os .DevNull , "--name" , "vcweb" , "--print-url" )
57103 cmd .Dir = dir
58104 cmd .Env = append (slices .Clip (env ), "PWD=" + dir )
59105
@@ -74,39 +120,32 @@ func (h *hgHandler) Handler(dir string, env []string, logger *log.Logger) (http.
74120
75121 stdout , err := cmd .StdoutPipe ()
76122 if err != nil {
123+ h .mu .Unlock ()
77124 http .Error (w , err .Error (), http .StatusInternalServerError )
78125 return
79126 }
80127
81128 if err := cmd .Start (); err != nil {
129+ h .mu .Unlock ()
82130 http .Error (w , err .Error (), http .StatusInternalServerError )
83131 return
84132 }
85- var wg sync.WaitGroup
86- defer func () {
87- cancel ()
88- err := cmd .Wait ()
89- if out := strings .TrimSuffix (stderr .String (), "interrupted!\n " ); out != "" {
90- logger .Printf ("%v: %v\n %s" , cmd , err , out )
91- } else {
92- logger .Printf ("%v" , cmd )
93- }
94- wg .Wait ()
95- }()
96133
97134 r := bufio .NewReader (stdout )
98135 line , err := r .ReadString ('\n' )
99136 if err != nil {
137+ h .mu .Unlock ()
138+ http .Error (w , err .Error (), http .StatusInternalServerError )
100139 return
101140 }
102141 // We have read what should be the server URL. 'hg serve' shouldn't need to
103142 // write anything else to stdout, but it's not a big deal if it does anyway.
104143 // Keep the stdout pipe open so that 'hg serve' won't get a SIGPIPE, but
105144 // actively discard its output so that it won't hang on a blocking write.
106- wg .Add (1 )
145+ h . wg .Add (1 )
107146 go func () {
108147 io .Copy (io .Discard , r )
109- wg .Done ()
148+ h . wg .Done ()
110149 }()
111150
112151 // On some systems,
@@ -116,12 +155,21 @@ func (h *hgHandler) Handler(dir string, env []string, logger *log.Logger) (http.
116155 line = strings .ReplaceAll (line , "//1.0.0.127.in-addr.arpa" , "//127.0.0.1" )
117156 line = strings .ReplaceAll (line , "//1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" , "//[::1]" )
118157
119- u , err : = url .Parse (strings .TrimSpace (line ))
158+ u , err = url .Parse (strings .TrimSpace (line ))
120159 if err != nil {
160+ h .mu .Unlock ()
121161 logger .Printf ("%v: %v" , cmd , err )
122162 http .Error (w , err .Error (), http .StatusBadGateway )
123163 return
124164 }
165+
166+ if h .url == nil {
167+ h .url = make (map [string ]* url.URL )
168+ }
169+ h .url [dir ] = u
170+ h .cmds = append (h .cmds , cmd )
171+ h .mu .Unlock ()
172+
125173 logger .Printf ("proxying hg request to %s" , u )
126174 httputil .NewSingleHostReverseProxy (u ).ServeHTTP (w , req )
127175 })
0 commit comments