diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 2c2d7872..ce619a23 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -531,6 +531,7 @@ type Hook struct { ResponseMessage string `json:"response-message,omitempty"` ResponseHeaders ResponseHeaders `json:"response-headers,omitempty"` CaptureCommandOutput bool `json:"include-command-output-in-response,omitempty"` + StreamCommandOutput bool `json:"stream-command-output,omitempty"` CaptureCommandOutputOnError bool `json:"include-command-output-in-response-on-error,omitempty"` PassEnvironmentToCommand []Argument `json:"pass-environment-to-command,omitempty"` PassArgumentsToCommand []Argument `json:"pass-arguments-to-command,omitempty"` diff --git a/webhook.go b/webhook.go index 6a258c41..a4589184 100644 --- a/webhook.go +++ b/webhook.go @@ -6,6 +6,7 @@ import ( "encoding/json" "flag" "fmt" + "io" "io/ioutil" "log" "net" @@ -67,6 +68,19 @@ var ( pidFile *pidfile.PIDFile ) +type flushWriter struct { + f http.Flusher + w io.Writer +} + +func (fw *flushWriter) Write(p []byte) (n int, err error) { + n, err = fw.w.Write(p) + if fw.f != nil { + fw.f.Flush() + } + return +} + func matchLoadedHook(id string) *hook.Hook { for _, hooks := range loadedHooksFromFiles { if hook := hooks.Match(id); hook != nil { @@ -535,8 +549,10 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set(responseHeader.Name, responseHeader.Value) } - if matchedHook.CaptureCommandOutput { - response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body) + if matchedHook.StreamCommandOutput { + handleHook(matchedHook, rid, &headers, &query, &payload, &body, w) + } else if matchedHook.CaptureCommandOutput { + response, err := handleHook(matchedHook, rid, &headers, &query, &payload, &body, nil) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -554,7 +570,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, response) } } else { - go handleHook(matchedHook, rid, &headers, &query, &payload, &body) + go handleHook(matchedHook, rid, &headers, &query, &payload, &body, nil) // Check if a success return code is configured for the hook if matchedHook.SuccessHttpResponseCode != 0 { @@ -577,7 +593,7 @@ func hookHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hook rules were not satisfied.") } -func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]interface{}, body *[]byte) (string, error) { +func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]interface{}, body *[]byte, w http.ResponseWriter) (string, error) { var errors []error // check the command exists @@ -646,12 +662,31 @@ func handleHook(h *hook.Hook, rid string, headers, query, payload *map[string]in log.Printf("[%s] executing %s (%s) with arguments %q and environment %s using %s as cwd\n", rid, h.ExecuteCommand, cmd.Path, cmd.Args, envs, cmd.Dir) - out, err := cmd.CombinedOutput() + var out []byte - log.Printf("[%s] command output: %s\n", rid, out) + if w != nil { + log.Printf("[%s] command output will be streamed to response", rid) - if err != nil { - log.Printf("[%s] error occurred: %+v\n", rid, err) + // Implementation from https://play.golang.org/p/PpbPyXbtEs + // as described in https://stackoverflow.com/questions/19292113/not-buffered-http-responsewritter-in-golang + fw := flushWriter{w: w} + if f, ok := w.(http.Flusher); ok { + fw.f = f + } + cmd.Stderr = &fw + cmd.Stdout = &fw + + if err := cmd.Run(); err != nil { + log.Printf("[%s] error occurred: %+v\n", rid, err) + } + } else { + out, err = cmd.CombinedOutput() + + log.Printf("[%s] command output: %s\n", rid, out) + + if err != nil { + log.Printf("[%s] error occurred: %+v\n", rid, err) + } } for i := range files { diff --git a/webhook_test.go b/webhook_test.go index 4671c245..4656ca9b 100644 --- a/webhook_test.go +++ b/webhook_test.go @@ -53,7 +53,7 @@ func TestStaticParams(t *testing.T) { b := &bytes.Buffer{} log.SetOutput(b) - _, err = handleHook(spHook, "test", &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{}) + _, err = handleHook(spHook, "test", &spHeaders, &map[string]interface{}{}, &map[string]interface{}{}, &[]byte{}, nil) if err != nil { t.Fatalf("Unexpected error: %v\n", err) }