Skip to content

Commit 1ee9961

Browse files
authored
Merge pull request #145 from xibz/fix-fifo-writer
Fixes issue where FifoLogWriter would hang
2 parents 6292bbe + 3d65a39 commit 1ee9961

File tree

4 files changed

+127
-118
lines changed

4 files changed

+127
-118
lines changed

fctesting/test_writer.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package fctesting
2+
3+
// TestWriter is used to mock out writing and/or do other things such as
4+
// syncing when to do assertions in the event that a writer is used in a
5+
// goroutine
6+
type TestWriter struct {
7+
WriteFn func([]byte) (int, error)
8+
}
9+
10+
func (w *TestWriter) Write(b []byte) (int, error) {
11+
return w.WriteFn(b)
12+
}

handlers_test.go

Lines changed: 0 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
package firecracker
22

33
import (
4-
"bytes"
54
"context"
65
"fmt"
76
"os"
87
"path/filepath"
98
"reflect"
109
"testing"
11-
"time"
1210

1311
models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
1412
ops "github.com/firecracker-microvm/firecracker-go-sdk/client/operations"
1513
"github.com/firecracker-microvm/firecracker-go-sdk/fctesting"
16-
"github.com/pkg/errors"
1714
)
1815

1916
func TestHandlerListAppend(t *testing.T) {
@@ -663,76 +660,6 @@ func TestHandlers(t *testing.T) {
663660
}
664661
}
665662

666-
func TestCreateLogFilesHandler(t *testing.T) {
667-
logWriterBuf := &bytes.Buffer{}
668-
config := Config{
669-
LogFifo: filepath.Join(testDataPath, "firecracker-log.fifo"),
670-
MetricsFifo: filepath.Join(testDataPath, "firecracker-metrics.fifo"),
671-
FifoLogWriter: logWriterBuf,
672-
}
673-
674-
defer func() {
675-
os.Remove(config.LogFifo)
676-
os.Remove(config.MetricsFifo)
677-
}()
678-
679-
ctx := context.Background()
680-
m, err := NewMachine(ctx, config, WithLogger(fctesting.NewLogEntry(t)))
681-
if err != nil {
682-
t.Fatalf("failed to create machine: %v", err)
683-
}
684-
685-
// spin off goroutine to write to Log fifo so we don't block
686-
doneChan := make(chan struct{}, 1)
687-
go func() {
688-
defer close(doneChan)
689-
690-
// try to open file
691-
fifoPipe, err := openFileRetry(config.LogFifo)
692-
if err != nil {
693-
t.Error(err)
694-
}
695-
696-
if _, err := fifoPipe.WriteString("data\n"); err != nil {
697-
t.Errorf("Failed to write to fifo %v", err)
698-
}
699-
700-
fifoPipe.Close()
701-
return
702-
}()
703-
704-
// Execute Handler
705-
if err := CreateLogFilesHandler.Fn(ctx, m); err != nil {
706-
t.Errorf("failed to call CreateLogFilesHandler function: %v", err)
707-
return
708-
}
709-
710-
// Block until writing go routine is done to check data that was written
711-
<-doneChan
712-
713-
// Poll for verifying logs were written as we need to allow time
714-
// for copying from the log fifo into the FifoLogWriter
715-
timer := time.NewTimer(1 * time.Second)
716-
for {
717-
select {
718-
case <-timer.C:
719-
t.Fatal("timed out reading from log writer")
720-
default:
721-
logData, err := logWriterBuf.ReadString('\n')
722-
if err != nil {
723-
time.Sleep(10 * time.Millisecond)
724-
continue
725-
}
726-
727-
if logData != "data\n" {
728-
t.Errorf("expected 'data' written to log got '%s'", logData)
729-
}
730-
return
731-
}
732-
}
733-
734-
}
735-
736663
func compareHandlerLists(l1, l2 HandlerList) bool {
737664
if l1.Len() != l2.Len() {
738665
return false
@@ -754,21 +681,3 @@ func compareHandlerLists(l1, l2 HandlerList) bool {
754681

755682
return true
756683
}
757-
758-
func openFileRetry(filePath string) (file *os.File, err error) {
759-
timer := time.NewTimer(1 * time.Second)
760-
for {
761-
select {
762-
case <-timer.C:
763-
err = errors.New("timed out waiting for file")
764-
return
765-
default:
766-
file, err = os.OpenFile(filePath, os.O_WRONLY, os.ModePerm)
767-
if err == nil {
768-
timer.Stop()
769-
return
770-
}
771-
time.Sleep(10 * time.Millisecond)
772-
}
773-
}
774-
}

machine.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -576,28 +576,37 @@ func (m *Machine) setupLogging(ctx context.Context) error {
576576
return nil
577577
}
578578

579-
func captureFifoToFile(logger *log.Entry, fifoPath string, fifo io.Writer) error {
580-
// create the fifo pipe which will be used
579+
func captureFifoToFile(logger *log.Entry, fifoPath string, w io.Writer) error {
580+
// open the fifo pipe which will be used
581581
// to write its contents to a file.
582-
fifoPipe, err := os.OpenFile(fifoPath, os.O_RDONLY, 0600)
582+
fd, err := syscall.Open(fifoPath, syscall.O_RDONLY|syscall.O_NONBLOCK, 0600)
583583
if err != nil {
584584
return fmt.Errorf("Failed to open fifo path at %q: %v", fifoPath, err)
585585
}
586586

587-
if err := syscall.Unlink(fifoPath); err != nil {
588-
logger.Warnf("Failed to unlink %s", fifoPath)
587+
fifoPipe := os.NewFile(uintptr(fd), fifoPath)
588+
if fifoPipe == nil {
589+
return fmt.Errorf("Invalid file descriptor")
589590
}
590591

591592
logger.Debugf("Capturing %q to writer", fifoPath)
592593

593-
// Uses a go routine to do a non-blocking io.Copy. The fifo
594-
// file should be closed when the appication has finished, since
595-
// the forked firecracker application will be closed resulting
596-
// in the pipe to return an io.EOF
594+
// Uses a goroutine to copy the contents of the fifo pipe to the io.Writer.
595+
// In the event that the goroutine finishes, which is caused by either the
596+
// context being closed or the application being closed, we will close the
597+
// pipe and unlink the fifo path.
597598
go func() {
598-
defer fifoPipe.Close()
599+
defer func() {
600+
if err := fifoPipe.Close(); err != nil {
601+
logger.Warnf("Failed to close fifo pipe: %v", err)
602+
}
603+
604+
if err := syscall.Unlink(fifoPath); err != nil {
605+
logger.Warnf("Failed to unlink %s: %v", fifoPath, err)
606+
}
607+
}()
599608

600-
if _, err := io.Copy(fifo, fifoPipe); err != nil {
609+
if _, err := io.Copy(w, fifoPipe); err != nil {
601610
logger.Warnf("io.Copy failed to copy contents of fifo pipe: %v", err)
602611
}
603612
}()

machine_test.go

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"path/filepath"
2626
"strconv"
2727
"strings"
28+
"sync"
2829
"syscall"
2930
"testing"
3031
"time"
@@ -146,7 +147,12 @@ func TestJailerMicroVMExecution(t *testing.T) {
146147
socketPath := filepath.Join(jailerTestPath, "firecracker", "TestJailerMicroVMExecution.socket")
147148
logFifo := filepath.Join(tmpDir, "firecracker.log")
148149
metricsFifo := filepath.Join(tmpDir, "firecracker-metrics")
150+
capturedLog := filepath.Join(tmpDir, "writer.fifo")
151+
fw, err := os.OpenFile(capturedLog, os.O_CREATE|os.O_RDWR, 0600)
152+
require.NoError(t, err, "failed to open fifo writer file")
149153
defer func() {
154+
fw.Close()
155+
os.Remove(capturedLog)
150156
os.Remove(socketPath)
151157
os.Remove(logFifo)
152158
os.Remove(metricsFifo)
@@ -183,6 +189,7 @@ func TestJailerMicroVMExecution(t *testing.T) {
183189
ExecFile: getFirecrackerBinaryPath(),
184190
ChrootStrategy: NewNaiveChrootStrategy(jailerFullRootPath, vmlinuxPath),
185191
},
192+
FifoLogWriter: fw,
186193
}
187194

188195
if _, err := os.Stat(vmlinuxPath); err != nil {
@@ -250,6 +257,10 @@ func TestJailerMicroVMExecution(t *testing.T) {
250257
}
251258

252259
m.StopVMM()
260+
261+
info, err := os.Stat(capturedLog)
262+
assert.NoError(t, err, "failed to stat captured log file")
263+
assert.NotEqual(t, 0, info.Size())
253264
}
254265

255266
func TestMicroVMExecution(t *testing.T) {
@@ -263,7 +274,12 @@ func TestMicroVMExecution(t *testing.T) {
263274
socketPath := filepath.Join(testDataPath, "TestMicroVMExecution.sock")
264275
logFifo := filepath.Join(testDataPath, "firecracker.log")
265276
metricsFifo := filepath.Join(testDataPath, "firecracker-metrics")
277+
capturedLog := filepath.Join(testDataPath, "writer.fifo")
278+
fw, err := os.OpenFile(capturedLog, os.O_CREATE|os.O_RDWR, 0600)
279+
require.NoError(t, err, "failed to open fifo writer file")
266280
defer func() {
281+
fw.Close()
282+
os.Remove(capturedLog)
267283
os.Remove(socketPath)
268284
os.Remove(logFifo)
269285
os.Remove(metricsFifo)
@@ -292,6 +308,7 @@ func TestMicroVMExecution(t *testing.T) {
292308
Debug: true,
293309
DisableValidation: true,
294310
NetworkInterfaces: networkIfaces,
311+
FifoLogWriter: fw,
295312
}
296313

297314
ctx := context.Background()
@@ -353,6 +370,10 @@ func TestMicroVMExecution(t *testing.T) {
353370
// didn't for some reason, we still need to terminate it:
354371
m.StopVMM()
355372
m.Wait(vmmCtx)
373+
374+
info, err := os.Stat(capturedLog)
375+
assert.NoError(t, err, "failed to stat captured log file")
376+
assert.NotEqual(t, 0, info.Size())
356377
}
357378

358379
func TestStartVMM(t *testing.T) {
@@ -774,7 +795,7 @@ func TestLogFiles(t *testing.T) {
774795
}
775796

776797
func TestCaptureFifoToFile(t *testing.T) {
777-
fifoPath := filepath.Join(testDataPath, "fifo")
798+
fifoPath := filepath.Join(testDataPath, "TestCaptureFifoToFile")
778799

779800
if err := syscall.Mkfifo(fifoPath, 0700); err != nil {
780801
t.Fatalf("Unexpected error during syscall.Mkfifo call: %v", err)
@@ -786,32 +807,90 @@ func TestCaptureFifoToFile(t *testing.T) {
786807
t.Fatalf("Failed to open file, %q: %v", fifoPath, err)
787808
}
788809

789-
f.Write([]byte("Hello world!"))
810+
expectedBytes := []byte("Hello world!")
811+
f.Write(expectedBytes)
790812
defer f.Close()
791813

792-
go func() {
793-
t := time.NewTicker(250 * time.Millisecond)
794-
select {
795-
case <-t.C:
796-
f.Close()
797-
}
798-
}()
814+
time.AfterFunc(250*time.Millisecond, func() { f.Close() })
815+
816+
logPath := fifoPath + ".log"
817+
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
818+
if err != nil {
819+
t.Fatalf("Failed to create log file: %v", err)
820+
}
821+
822+
var wg sync.WaitGroup
823+
wg.Add(1)
824+
825+
testWriter := &fctesting.TestWriter{
826+
WriteFn: func(b []byte) (int, error) {
827+
defer wg.Done()
828+
829+
return logFile.Write(b)
830+
},
831+
}
832+
833+
if err := captureFifoToFile(fctesting.NewLogEntry(t), fifoPath, testWriter); err != nil {
834+
t.Errorf("Unexpected error: %v", err)
835+
}
836+
837+
defer os.Remove(logPath)
838+
839+
wg.Wait()
840+
_, err = os.Stat(logPath)
841+
assert.NoError(t, err, "failed to stat file")
842+
b, err := ioutil.ReadFile(logPath)
843+
assert.NoError(t, err, "failed to read logPath")
844+
assert.Equal(t, expectedBytes, b)
845+
}
846+
847+
func TestCaptureFifoToFile_nonblock(t *testing.T) {
848+
fifoPath := filepath.Join(testDataPath, "TestCaptureFifoToFile_nonblock")
799849

800-
fifoLogPath := fifoPath + ".log"
801-
fifo, err := os.OpenFile(fifoLogPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
850+
if err := syscall.Mkfifo(fifoPath, 0700); err != nil {
851+
t.Fatalf("Unexpected error during syscall.Mkfifo call: %v", err)
852+
}
853+
defer os.Remove(fifoPath)
854+
855+
logPath := fifoPath + ".log"
856+
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
802857
if err != nil {
803-
t.Fatalf("Failed to create fifo file: %v", err)
858+
t.Fatalf("Failed to create log file: %v", err)
859+
}
860+
861+
var wg sync.WaitGroup
862+
wg.Add(1)
863+
864+
testWriter := &fctesting.TestWriter{
865+
WriteFn: func(b []byte) (int, error) {
866+
defer wg.Done()
867+
868+
return logFile.Write(b)
869+
},
804870
}
805871

806-
if err := captureFifoToFile(fctesting.NewLogEntry(t), fifoPath, fifo); err != nil {
872+
if err := captureFifoToFile(fctesting.NewLogEntry(t), fifoPath, testWriter); err != nil {
807873
t.Errorf("Unexpected error: %v", err)
808874
}
809875

810-
defer os.Remove(fifoLogPath)
876+
defer os.Remove(logPath)
811877

812-
if _, err := os.Stat(fifoLogPath); err != nil {
813-
t.Errorf("Failed to stat file: %v", err)
878+
f, err := os.OpenFile(fifoPath, os.O_RDWR, 0600)
879+
if err != nil {
880+
t.Fatalf("Failed to open file, %q: %v", fifoPath, err)
814881
}
882+
expectedBytes := []byte("Hello world!")
883+
f.Write(expectedBytes)
884+
defer f.Close()
885+
886+
time.AfterFunc(250*time.Millisecond, func() { f.Close() })
887+
888+
wg.Wait()
889+
_, err = os.Stat(logPath)
890+
assert.NoError(t, err, "failed to stat file")
891+
b, err := ioutil.ReadFile(logPath)
892+
assert.NoError(t, err, "failed to read logPath")
893+
assert.Equal(t, expectedBytes, b)
815894
}
816895

817896
func TestSocketPathSet(t *testing.T) {

0 commit comments

Comments
 (0)