Skip to content

Commit baf75b9

Browse files
committed
Merge remote-tracking branch 'origin/pr/57'
2 parents e4d80c1 + c26c185 commit baf75b9

File tree

2 files changed

+134
-69
lines changed

2 files changed

+134
-69
lines changed

machine.go

Lines changed: 65 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os/exec"
2323
"os/signal"
2424
"strconv"
25+
"sync"
2526
"syscall"
2627
"time"
2728

@@ -34,6 +35,8 @@ const (
3435
userAgent = "firecracker-go-sdk"
3536
)
3637

38+
var ErrAlreadyStarted = errors.New("firecracker: machine already started")
39+
3740
// Firecracker is an interface that can be used to mock
3841
// out an Firecracker agent for testing purposes.
3942
type Firecracker interface {
@@ -134,17 +137,23 @@ func (cfg *Config) Validate() error {
134137

135138
// Machine is the main object for manipulating Firecracker microVMs
136139
type Machine struct {
140+
// Metadata is the associated metadata that will be sent to the firecracker
141+
// process
142+
Metadata interface{}
143+
// Handlers holds the set of handlers that are run for validation and start
144+
Handlers Handlers
145+
137146
cfg Config
138147
client Firecracker
139148
cmd *exec.Cmd
140149
logger *log.Entry
141150
machineConfig models.MachineConfiguration // The actual machine config as reported by Firecracker
142-
143-
// Metadata is the associated metadata that will be sent to the firecracker
144-
// process
145-
Metadata interface{}
146-
errCh chan error
147-
Handlers Handlers
151+
// startOnce ensures that the machine can only be started once
152+
startOnce sync.Once
153+
// exitCh is a channel which gets closed when the VMM exits
154+
exitCh chan struct{}
155+
// err records any error executing the VMM
156+
err error
148157
}
149158

150159
// Logger returns a logrus logger appropriate for logging hypervisor messages
@@ -196,7 +205,9 @@ func NewMachine(ctx context.Context, cfg Config, opts ...Opt) (*Machine, error)
196205
return nil, err
197206
}
198207

199-
m := &Machine{}
208+
m := &Machine{
209+
exitCh: make(chan struct{}),
210+
}
200211
logger := log.New()
201212

202213
if cfg.Debug {
@@ -226,22 +237,35 @@ func NewMachine(ctx context.Context, cfg Config, opts ...Opt) (*Machine, error)
226237
// Start will iterate through the handler list and call each handler. If an
227238
// error occurred during handler execution, that error will be returned. If the
228239
// handlers succeed, then this will start the VMM instance.
240+
// Start may only be called once per Machine. Subsequent calls will return
241+
// ErrAlreadyStarted.
229242
func (m *Machine) Start(ctx context.Context) error {
230243
m.logger.Debug("Called Machine.Start()")
244+
alreadyStarted := true
245+
m.startOnce.Do(func() {
246+
m.logger.Debug("Marking Machine as Started")
247+
alreadyStarted = false
248+
})
249+
if alreadyStarted {
250+
return ErrAlreadyStarted
251+
}
252+
231253
if err := m.Handlers.Run(ctx, m); err != nil {
232254
return err
233255
}
234256

235-
return m.StartInstance(ctx)
257+
return m.startInstance(ctx)
236258
}
237259

238-
// Wait will wait until the firecracker process has finished
260+
// Wait will wait until the firecracker process has finished. Wait is safe to
261+
// call concurrently, and will deliver the same error to all callers, subject to
262+
// each caller's context cancellation.
239263
func (m *Machine) Wait(ctx context.Context) error {
240264
select {
241265
case <-ctx.Done():
242266
return ctx.Err()
243-
case err := <-m.errCh:
244-
return err
267+
case <-m.exitCh:
268+
return m.err
245269
}
246270
}
247271

@@ -281,11 +305,12 @@ func (m *Machine) attachDrives(ctx context.Context, drives ...models.Drive) erro
281305
func (m *Machine) startVMM(ctx context.Context) error {
282306
m.logger.Printf("Called startVMM(), setting up a VMM on %s", m.cfg.SocketPath)
283307

284-
m.errCh = make(chan error)
308+
errCh := make(chan error)
285309

286310
err := m.cmd.Start()
287311
if err != nil {
288312
m.logger.Errorf("Failed to start VMM: %s", err)
313+
close(m.exitCh)
289314
return err
290315
}
291316
m.logger.Debugf("VMM started socket path is %s", m.cfg.SocketPath)
@@ -300,11 +325,10 @@ func (m *Machine) startVMM(ctx context.Context) error {
300325
os.Remove(m.cfg.SocketPath)
301326
os.Remove(m.cfg.LogFifo)
302327
os.Remove(m.cfg.MetricsFifo)
303-
m.errCh <- err
328+
errCh <- err
304329
}()
305330

306331
// Set up a signal handler and pass INT, QUIT, and TERM through to firecracker
307-
vmchan := make(chan error)
308332
sigchan := make(chan os.Signal)
309333
signal.Notify(sigchan, os.Interrupt,
310334
syscall.SIGQUIT,
@@ -313,22 +337,24 @@ func (m *Machine) startVMM(ctx context.Context) error {
313337
syscall.SIGABRT)
314338
m.logger.Debugf("Setting up signal handler")
315339
go func() {
316-
select {
317-
case sig := <-sigchan:
318-
m.logger.Printf("Caught signal %s", sig)
319-
m.cmd.Process.Signal(sig)
320-
case err = <-vmchan:
321-
m.errCh <- err
322-
}
340+
sig := <-sigchan
341+
m.logger.Printf("Caught signal %s", sig)
342+
m.cmd.Process.Signal(sig)
323343
}()
324344

325345
// Wait for firecracker to initialize:
326-
err = m.waitForSocket(3*time.Second, m.errCh)
346+
err = m.waitForSocket(3*time.Second, errCh)
327347
if err != nil {
328348
msg := fmt.Sprintf("Firecracker did not create API socket %s: %s", m.cfg.SocketPath, err)
329349
err = errors.New(msg)
350+
close(m.exitCh)
330351
return err
331352
}
353+
go func() {
354+
err := <-errCh
355+
m.err = err
356+
close(m.exitCh)
357+
}()
332358

333359
m.logger.Debugf("returning from startVMM()")
334360
return nil
@@ -517,11 +543,6 @@ func (m *Machine) addVsock(ctx context.Context, dev VsockDevice) error {
517543
return nil
518544
}
519545

520-
// StartInstance starts the Firecracker microVM
521-
func (m *Machine) StartInstance(ctx context.Context) error {
522-
return m.startInstance(ctx)
523-
}
524-
525546
func (m *Machine) startInstance(ctx context.Context) error {
526547
info := models.InstanceActionInfo{
527548
ActionType: models.InstanceActionInfoActionTypeInstanceStart,
@@ -575,33 +596,25 @@ func (m *Machine) waitForSocket(timeout time.Duration, exitchan chan error) erro
575596
ctx, cancel := context.WithTimeout(context.Background(), timeout)
576597
defer cancel()
577598

578-
done := make(chan error)
579599
ticker := time.NewTicker(10 * time.Millisecond)
580600

581-
go func() {
582-
for {
583-
select {
584-
case <-ctx.Done():
585-
done <- ctx.Err()
586-
return
587-
case err := <-exitchan:
588-
done <- err
589-
return
590-
case <-ticker.C:
591-
if _, err := os.Stat(m.cfg.SocketPath); err != nil {
592-
continue
593-
}
594-
595-
// Send test HTTP request to make sure socket is available
596-
if _, err := m.client.GetMachineConfig(); err != nil {
597-
continue
598-
}
599-
600-
done <- nil
601-
return
601+
for {
602+
select {
603+
case <-ctx.Done():
604+
return ctx.Err()
605+
case err := <-exitchan:
606+
return err
607+
case <-ticker.C:
608+
if _, err := os.Stat(m.cfg.SocketPath); err != nil {
609+
continue
602610
}
603-
}
604-
}()
605611

606-
return <-done
612+
// Send test HTTP request to make sure socket is available
613+
if _, err := m.client.GetMachineConfig(); err != nil {
614+
continue
615+
}
616+
617+
return nil
618+
}
619+
}
607620
}

machine_test.go

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/firecracker-microvm/firecracker-go-sdk/fctesting"
3535
"github.com/golang/mock/gomock"
3636
log "github.com/sirupsen/logrus"
37+
"github.com/stretchr/testify/assert"
3738
)
3839

3940
const (
@@ -92,13 +93,7 @@ func TestMicroVMExecution(t *testing.T) {
9293
os.Remove(metricsFifo)
9394
}()
9495

95-
vmlinuxPath := filepath.Join(testDataPath, "./vmlinux")
96-
if _, err := os.Stat(vmlinuxPath); err != nil {
97-
t.Fatalf("Cannot find vmlinux file: %s\n"+
98-
`Verify that you have a vmlinux file at "%s" or set the `+
99-
"`%s` environment variable to the correct location.",
100-
err, vmlinuxPath, testDataPathEnv)
101-
}
96+
vmlinuxPath := getVmlinuxPath(t)
10297

10398
cfg := Config{
10499
SocketPath: socketPath,
@@ -183,19 +178,64 @@ func TestStartVMM(t *testing.T) {
183178
defer cancel()
184179
err = m.startVMM(timeout)
185180
if err != nil {
186-
t.Errorf("startVMM failed: %s", err)
187-
} else {
188-
defer m.StopVMM()
181+
t.Fatalf("startVMM failed: %s", err)
182+
}
183+
defer m.StopVMM()
189184

190-
select {
191-
case <-timeout.Done():
192-
if timeout.Err() == context.DeadlineExceeded {
193-
t.Log("firecracker ran for 250ms")
194-
} else {
195-
t.Errorf("startVMM returned %s", m.Wait(ctx))
196-
}
185+
select {
186+
case <-timeout.Done():
187+
if timeout.Err() == context.DeadlineExceeded {
188+
t.Log("firecracker ran for 250ms")
189+
} else {
190+
t.Errorf("startVMM returned %s", m.Wait(ctx))
197191
}
198192
}
193+
194+
}
195+
196+
func TestStartVMMOnce(t *testing.T) {
197+
socketPath := filepath.Join("testdata", "fc-start-vmm-test.sock")
198+
defer os.Remove(socketPath)
199+
200+
cfg := Config{
201+
SocketPath: socketPath,
202+
DisableValidation: true,
203+
KernelImagePath: getVmlinuxPath(t),
204+
MachineCfg: models.MachineConfiguration{
205+
VcpuCount: 1,
206+
},
207+
}
208+
ctx := context.Background()
209+
cmd := VMCommandBuilder{}.
210+
WithSocketPath(cfg.SocketPath).
211+
WithBin(getFirecrackerBinaryPath()).
212+
Build(ctx)
213+
m, err := NewMachine(ctx, cfg, WithProcessRunner(cmd))
214+
if err != nil {
215+
t.Fatalf("unexpected error: %v", err)
216+
}
217+
defer m.StopVMM()
218+
219+
timeout, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
220+
defer cancel()
221+
err = m.Start(timeout)
222+
if err != nil {
223+
t.Fatalf("startVMM failed: %s", err)
224+
}
225+
defer m.StopVMM()
226+
err = m.Start(timeout)
227+
assert.Error(t, err, "should return an error when Start is called multiple times")
228+
assert.Equal(t, ErrAlreadyStarted, err, "should be ErrAlreadyStarted")
229+
230+
select {
231+
case <-timeout.Done():
232+
if timeout.Err() == context.DeadlineExceeded {
233+
t.Log("firecracker ran for 250ms")
234+
} else {
235+
t.Errorf("startVMM returned %s", m.Wait(ctx))
236+
}
237+
}
238+
199239
}
200240

201241
func getFirecrackerBinaryPath() string {
@@ -205,6 +245,18 @@ func getFirecrackerBinaryPath() string {
205245
return filepath.Join(testDataPath, firecrackerBinaryPath)
206246
}
207247

248+
func getVmlinuxPath(t *testing.T) string {
249+
t.Helper()
250+
vmlinuxPath := filepath.Join(testDataPath, "./vmlinux")
251+
if _, err := os.Stat(vmlinuxPath); err != nil {
252+
t.Fatalf("Cannot find vmlinux file: %s\n"+
253+
`Verify that you have a vmlinux file at "%s" or set the `+
254+
"`%s` environment variable to the correct location.",
255+
err, vmlinuxPath, testDataPathEnv)
256+
}
257+
return vmlinuxPath
258+
}
259+
208260
func testCreateMachine(ctx context.Context, t *testing.T, m *Machine) {
209261
err := m.createMachine(ctx)
210262
if err != nil {

0 commit comments

Comments
 (0)