-
Notifications
You must be signed in to change notification settings - Fork 133
fix: prevent indefinite hang in long-running scans with periodic flush #942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package output | ||
|
|
||
| import ( | ||
| "os" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestFileWriterConcurrent(t *testing.T) { | ||
| // Create temporary file | ||
| tmpfile, err := os.CreateTemp("", "test-*.jsonl") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| defer os.Remove(tmpfile.Name()) | ||
|
|
||
|
Comment on lines
+10
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n pkg/output/file_writer_test.goRepository: projectdiscovery/tlsx Length of output: 2286 🏁 Script executed: find . -name "*.go" -path "*/output/*" | head -20Repository: projectdiscovery/tlsx Length of output: 204 🏁 Script executed: cat -n pkg/output/file_writer.goRepository: projectdiscovery/tlsx Length of output: 1898 Close temp-file descriptors explicitly after Both tests keep the 🤖 Prompt for AI Agents |
||
| writer, err := newFileOutputWriter(tmpfile.Name()) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| defer writer.Close() | ||
|
|
||
| // Test concurrent writes | ||
| done := make(chan bool, 10) | ||
| for i := 0; i < 10; i++ { | ||
| go func(id int) { | ||
| for j := 0; j < 100; j++ { | ||
| data := []byte(`{"test": "data"}`) | ||
| if err := writer.Write(data); err != nil { | ||
| t.Errorf("Write failed: %v", err) | ||
| return | ||
| } | ||
| } | ||
| done <- true | ||
| }(i) | ||
| } | ||
|
|
||
| // Wait for all goroutines to complete | ||
| for i := 0; i < 10; i++ { | ||
| <-done | ||
| } | ||
|
|
||
| // Verify file was written | ||
| info, err := tmpfile.Stat() | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| if info.Size() == 0 { | ||
| t.Error("Expected file to have content") | ||
| } | ||
|
Comment on lines
+23
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "file_writer_test.go" | head -5Repository: projectdiscovery/tlsx Length of output: 97 🏁 Script executed: cat -n pkg/output/file_writer_test.go | head -100Repository: projectdiscovery/tlsx Length of output: 2286 🏁 Script executed: find . -type f -name "*.go" -path "*/output/*" | head -20Repository: projectdiscovery/tlsx Length of output: 204 🏁 Script executed: cat -n pkg/output/file_writer.goRepository: projectdiscovery/tlsx Length of output: 1898
When Use 💡 Suggested fix import (
+ "bytes"
"os"
+ "sync"
"testing"
)
@@
- // Test concurrent writes
- done := make(chan bool, 10)
- for i := 0; i < 10; i++ {
- go func(id int) {
- for j := 0; j < 100; j++ {
- data := []byte(`{"test": "data"}`)
- if err := writer.Write(data); err != nil {
- t.Errorf("Write failed: %v", err)
- return
- }
- }
- done <- true
- }(i)
- }
-
- // Wait for all goroutines to complete
- for i := 0; i < 10; i++ {
- <-done
- }
-
- // Verify file was written
- info, err := tmpfile.Stat()
+ const goroutines = 10
+ const writesPerGoroutine = 100
+ var wg sync.WaitGroup
+ errCh := make(chan error, goroutines*writesPerGoroutine)
+
+ for i := 0; i < goroutines; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := 0; j < writesPerGoroutine; j++ {
+ if err := writer.Write([]byte(`{"test": "data"}`)); err != nil {
+ errCh <- err
+ return
+ }
+ }
+ }()
+ }
+
+ wg.Wait()
+ close(errCh)
+ for err := range errCh {
+ t.Fatalf("Write failed: %v", err)
+ }
+
+ if err := writer.Close(); err != nil {
+ t.Fatal(err)
+ }
+
+ content, err := os.ReadFile(tmpfile.Name())
if err != nil {
t.Fatal(err)
}
-
- if info.Size() == 0 {
- t.Error("Expected file to have content")
+
+ gotLines := bytes.Count(content, []byte("\n"))
+ wantLines := goroutines * writesPerGoroutine
+ if gotLines != wantLines {
+ t.Fatalf("expected %d lines, got %d", wantLines, gotLines)
}
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| func TestFileWriterFlush(t *testing.T) { | ||
| tmpfile, err := os.CreateTemp("", "test-flush-*.jsonl") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| defer os.Remove(tmpfile.Name()) | ||
|
|
||
| writer, err := newFileOutputWriter(tmpfile.Name()) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| // Write data | ||
| data := []byte(`{"test": "flush"}`) | ||
| if err := writer.Write(data); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| // Data should be flushed to disk | ||
| if err := writer.Close(); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| // Read file and verify | ||
| content, err := os.ReadFile(tmpfile.Name()) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| expected := `{"test": "flush"}` + "\n" | ||
| if string(content) != expected { | ||
| t.Errorf("Expected %q, got %q", expected, string(content)) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: projectdiscovery/tlsx
Length of output: 92
🏁 Script executed:
Repository: projectdiscovery/tlsx
Length of output: 1898
Propagate
Sync()failures instead of suppressing them.Syncerrors are currently ignored on lines 47–49 and 62, so callers can receivenileven when data persistence failed (e.g., disk full / I/O error). Update both theWrite()andClose()methods to check and returnSync()errors.Suggested fix
func (w *fileWriter) Write(data []byte) error { w.mu.Lock() defer w.mu.Unlock() _, err := w.writer.Write(data) if err != nil { return err } _, err = w.writer.WriteRune('\n') if err != nil { return err } // Flush periodically to prevent buffer deadlock in long-running scans // This fixes issue `#819` where tlsx hangs after ~25k targets err = w.writer.Flush() if err != nil { return err } // Sync to disk to prevent data loss on crash - //nolint:errcheck // we don't care whether sync failed or succeeded. - w.file.Sync() + if err := w.file.Sync(); err != nil { + return err + } return nil } // Close closes the underlying writer flushing everything to disk func (w *fileWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() if err := w.writer.Flush(); err != nil { return err } - //nolint:errcheck // we don't care whether sync failed or succeeded. - w.file.Sync() - return w.file.Close() + if err := w.file.Sync(); err != nil { + return err + } + return w.file.Close() }📝 Committable suggestion
🤖 Prompt for AI Agents