Skip to content

Commit e9734c1

Browse files
Fix pre-receive hook hangs and missing logs by flushing logs on signal and using CommandContext for git commands (#4714)
When TruffleHog times out in pre-receive hooks, it fails to output diagnostic logs. Changes: - Ensure log output is flushed before process termination in all exit paths: * Defer log sync in run() function for normal exits * Sync logs in signal handler before os.Exit(0) * Sync logs before os.Exit(183) when results are found * Sync logs in logFatalFunc before os.Exit() calls - Use exec.CommandContext instead of exec.Command for git log/diff to ensure processes are killed when context is cancelled - Add WaitDelay to git commands to provide grace period for cleanup This ensures diagnostic output is captured when git operations block in pre-receive hook environments and logs are visible when process is killed.
1 parent 7635b24 commit e9734c1

File tree

3 files changed

+62
-14
lines changed

3 files changed

+62
-14
lines changed

main.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"strings"
1515
"sync"
1616
"syscall"
17-
1817
"github.com/alecthomas/kingpin/v2"
1918
"github.com/fatih/color"
2019
"github.com/felixge/fgprof"
@@ -347,6 +346,13 @@ func init() {
347346
}
348347
}
349348

349+
// syncLogs flushes logs when the program exits.
350+
func syncLogs(syncFn func() error) {
351+
if syncFn != nil {
352+
_ = syncFn()
353+
}
354+
}
355+
350356
func main() {
351357
// setup logger
352358
logFormat := log.WithConsoleSink
@@ -358,15 +364,16 @@ func main() {
358364
context.SetDefaultLogger(logger)
359365

360366
if *localDev {
361-
run(overseer.State{})
367+
run(overseer.State{}, sync)
362368
os.Exit(0)
363369
}
364370

365-
defer func() { _ = sync() }()
366-
logFatal := logFatalFunc(logger)
371+
logFatal := logFatalFunc(logger, sync)
367372

368373
updateCfg := overseer.Config{
369-
Program: run,
374+
Program: func(s overseer.State) {
375+
run(s, sync)
376+
},
370377
Debug: *debug,
371378
RestartSignal: syscall.SIGTERM,
372379
// TODO: Eventually add a PreUpgrade func for signature check w/ x509 PKCS1v15
@@ -387,10 +394,10 @@ func main() {
387394
}
388395
}
389396

390-
func run(state overseer.State) {
391-
397+
func run(state overseer.State, logSync func() error) {
392398
ctx, cancel := context.WithCancelCause(context.Background())
393399
defer cancel(nil)
400+
defer syncLogs(logSync)
394401

395402
go func() {
396403
if err := cleantemp.CleanTempArtifacts(ctx); err != nil {
@@ -399,7 +406,7 @@ func run(state overseer.State) {
399406
}()
400407

401408
logger := ctx.Logger()
402-
logFatal := logFatalFunc(logger)
409+
logFatal := logFatalFunc(logger, logSync)
403410

404411
killSignal := make(chan os.Signal, 1)
405412
signal.Notify(killSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
@@ -413,6 +420,8 @@ func run(state overseer.State) {
413420
} else {
414421
logger.Info("cleaned temporary artifacts")
415422
}
423+
424+
syncLogs(logSync)
416425
os.Exit(0)
417426
}()
418427

@@ -606,6 +615,7 @@ func run(state overseer.State) {
606615

607616
if metrics.hasFoundResults && *fail {
608617
logger.V(2).Info("exiting with code 183 because results were found")
618+
syncLogs(logSync)
609619
os.Exit(183)
610620
}
611621
}
@@ -1165,9 +1175,10 @@ func parseResults(input *string) (map[string]struct{}, error) {
11651175

11661176
// logFatalFunc returns a log.Fatal style function. Calling the returned
11671177
// function will terminate the program without cleanup.
1168-
func logFatalFunc(logger logr.Logger) func(error, string, ...any) {
1178+
func logFatalFunc(logger logr.Logger, logSync func() error) func(error, string, ...any) {
11691179
return func(err error, message string, keyAndVals ...any) {
11701180
logger.Error(err, message, keyAndVals...)
1181+
syncLogs(logSync)
11711182
if err != nil {
11721183
os.Exit(1)
11731184
return

pkg/gitparse/gitparse.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const (
3030

3131
// defaultMaxCommitSize is the maximum size for a commit. Larger commits will be cut off.
3232
defaultMaxCommitSize int64 = 2 * 1024 * 1024 * 1024 // 2GB
33+
34+
// defaultWaitDelay is the default time to wait after context cancellation before forcefully killing git processes.
35+
defaultWaitDelay = 5 * time.Second
3336
)
3437

3538
// contentWriter defines a common interface for writing, reading, and managing diff content.
@@ -124,6 +127,7 @@ type Parser struct {
124127
maxDiffSize int64
125128
maxCommitSize int64
126129
dateFormat string
130+
waitDelay time.Duration
127131

128132
useCustomContentWriter bool
129133
}
@@ -204,6 +208,14 @@ func WithMaxCommitSize(maxCommitSize int64) Option {
204208
}
205209
}
206210

211+
// WithWaitDelay sets the waitDelay option. This specifies how long to wait after
212+
// context cancellation before forcefully killing git processes.
213+
func WithWaitDelay(waitDelay time.Duration) Option {
214+
return func(parser *Parser) {
215+
parser.waitDelay = waitDelay
216+
}
217+
}
218+
207219
// Option is used for adding options to Config.
208220
type Option func(*Parser)
209221

@@ -213,6 +225,7 @@ func NewParser(options ...Option) *Parser {
213225
dateFormat: defaultDateFormat,
214226
maxDiffSize: defaultMaxDiffSize,
215227
maxCommitSize: defaultMaxCommitSize,
228+
waitDelay: defaultWaitDelay,
216229
}
217230
for _, option := range options {
218231
option(parser)
@@ -253,7 +266,7 @@ func (c *Parser) RepoPath(
253266
args = append(args, "--", ".", ":(exclude)"+glob)
254267
}
255268

256-
cmd := exec.Command("git", args...)
269+
cmd := exec.CommandContext(ctx, "git", args...)
257270
absPath, err := filepath.Abs(source)
258271
if err == nil {
259272
if !isBare {
@@ -273,26 +286,27 @@ func (c *Parser) RepoPath(
273286
}
274287
}
275288

276-
return c.executeCommand(ctx, cmd, false)
289+
return c.executeCommand(ctx, cmd, false, c.waitDelay)
277290
}
278291

279292
// Staged parses the output of the `git diff` command for the `source` path.
280293
func (c *Parser) Staged(ctx context.Context, source string) (chan *Diff, error) {
281294
// Provide the --cached flag to diff to get the diff of the staged changes.
282295
args := []string{"-C", source, "diff", "-p", "--cached", "--full-history", "--diff-filter=AM", "--date=iso-strict"}
283296

284-
cmd := exec.Command("git", args...)
297+
cmd := exec.CommandContext(ctx, "git", args...)
285298

286299
absPath, err := filepath.Abs(source)
287300
if err == nil {
288301
cmd.Env = append(cmd.Env, "GIT_DIR="+filepath.Join(absPath, ".git"))
289302
}
290303

291-
return c.executeCommand(ctx, cmd, true)
304+
return c.executeCommand(ctx, cmd, true, c.waitDelay)
292305
}
293306

294307
// executeCommand runs an exec.Cmd, reads stdout and stderr, and waits for the Cmd to complete.
295-
func (c *Parser) executeCommand(ctx context.Context, cmd *exec.Cmd, isStaged bool) (chan *Diff, error) {
308+
// waitDelay specifies how long to wait after context cancellation before forcefully killing the process.
309+
func (c *Parser) executeCommand(ctx context.Context, cmd *exec.Cmd, isStaged bool, waitDelay time.Duration) (chan *Diff, error) {
296310
diffChan := make(chan *Diff, 64)
297311

298312
stdOut, err := cmd.StdoutPipe()
@@ -304,6 +318,9 @@ func (c *Parser) executeCommand(ctx context.Context, cmd *exec.Cmd, isStaged boo
304318
return diffChan, err
305319
}
306320

321+
// Set WaitDelay to allow the command additional time to exit after context cancellation
322+
cmd.WaitDelay = waitDelay
323+
307324
err = cmd.Start()
308325
if err != nil {
309326
return diffChan, err

pkg/gitparse/gitparse_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,26 @@ func TestMaxCommitSize(t *testing.T) {
14781478
}
14791479

14801480
}
1481+
func TestWaitDelay(t *testing.T) {
1482+
// Test that WithWaitDelay sets the waitDelay correctly
1483+
customDelay := 10 * time.Second
1484+
parser := NewParser(WithWaitDelay(customDelay))
1485+
if parser.waitDelay != customDelay {
1486+
t.Errorf("waitDelay not set correctly. Got: %v, expected: %v", parser.waitDelay, customDelay)
1487+
}
1488+
1489+
// Test that default waitDelay is used when not specified
1490+
defaultParser := NewParser()
1491+
if defaultParser.waitDelay != defaultWaitDelay {
1492+
t.Errorf("default waitDelay not set correctly. Got: %v, expected: %v", defaultParser.waitDelay, defaultWaitDelay)
1493+
}
1494+
1495+
// Test that zero value is allowed (caller can set to 0 if they want no delay)
1496+
zeroDelayParser := NewParser(WithWaitDelay(0))
1497+
if zeroDelayParser.waitDelay != 0 {
1498+
t.Errorf("zero waitDelay not set correctly. Got: %v, expected: 0", zeroDelayParser.waitDelay)
1499+
}
1500+
}
14811501

14821502
const commitLog = `commit e50b135fd29e91b2fbb25923797f5ecffe59f359
14831503
Author: lionzxy <nikita@kulikof.ru>

0 commit comments

Comments
 (0)