-
-
Notifications
You must be signed in to change notification settings - Fork 50
/
virtualization.go
495 lines (431 loc) · 16.1 KB
/
virtualization.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
package vz
/*
#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc
#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework Virtualization -framework Cocoa
# include "virtualization_11.h"
# include "virtualization_12.h"
# include "virtualization_13.h"
*/
import "C"
import (
"fmt"
"runtime/cgo"
"sync"
"unsafe"
infinity "github.com/Code-Hex/go-infinity-channel"
"github.com/Code-Hex/vz/v3/internal/objc"
"github.com/Code-Hex/vz/v3/internal/sliceutil"
)
// VirtualMachineState represents execution state of the virtual machine.
//
//go:generate stringer -type=VirtualMachineState
type VirtualMachineState int
const (
// VirtualMachineStateStopped Initial state before the virtual machine is started.
VirtualMachineStateStopped VirtualMachineState = iota
// VirtualMachineStateRunning Running virtual machine.
VirtualMachineStateRunning
// VirtualMachineStatePaused A started virtual machine is paused.
// This state can only be transitioned from VirtualMachineStatePausing.
VirtualMachineStatePaused
// VirtualMachineStateError The virtual machine has encountered an internal error.
VirtualMachineStateError
// VirtualMachineStateStarting The virtual machine is configuring the hardware and starting.
VirtualMachineStateStarting
// VirtualMachineStatePausing The virtual machine is being paused.
// This is the intermediate state between VirtualMachineStateRunning and VirtualMachineStatePaused.
VirtualMachineStatePausing
// VirtualMachineStateResuming The virtual machine is being resumed.
// This is the intermediate state between VirtualMachineStatePaused and VirtualMachineStateRunning.
VirtualMachineStateResuming
// VirtualMachineStateStopping The virtual machine is being stopped.
// This is the intermediate state between VirtualMachineStateRunning and VirtualMachineStateStop.
//
// Available on macOS 12.0 and above.
VirtualMachineStateStopping
// VirtualMachineStateSaving The virtual machine is being saved.
// This is the intermediate state between VirtualMachineStatePaused and VirtualMachineStatePaused
//
// Available on macOS 14.0 and above.
VirtualMachineStateSaving
// VirtualMachineStateRestoring The virtual machine is being restored.
// This is the intermediate state between VirtualMachineStateStopped and either VirtualMachineStatePaused on success or VirtualMachineStateStopped on failure.
//
// Available on macOS 14.0 and above.
VirtualMachineStateRestoring
)
// VirtualMachine represents the entire state of a single virtual machine.
//
// A Virtual Machine is the emulation of a complete hardware machine of the same architecture as the real hardware machine.
// When executing the Virtual Machine, the Virtualization framework uses certain hardware resources and emulates others to provide isolation
// and great performance.
//
// The definition of a virtual machine starts with its configuration. This is done by setting up a VirtualMachineConfiguration struct.
// Once configured, the virtual machine can be started with (*VirtualMachine).Start() method.
//
// Creating a virtual machine using the Virtualization framework requires the app to have the "com.apple.security.virtualization" entitlement.
// see: https://developer.apple.com/documentation/virtualization/vzvirtualmachine?language=objc
type VirtualMachine struct {
// id for this struct.
id string
// Indicate whether or not virtualization is available.
//
// If virtualization is unavailable, no VirtualMachineConfiguration will validate.
// The validation error of the VirtualMachineConfiguration provides more information about why virtualization is unavailable.
supported bool
*pointer
dispatchQueue unsafe.Pointer
machineState *machineState
disconnectedIn *infinity.Channel[*disconnected]
disconnectedOut *infinity.Channel[*DisconnectedError]
watchDisconnectedOnce sync.Once
finalizeOnce sync.Once
config *VirtualMachineConfiguration
mu sync.RWMutex
}
type machineState struct {
state VirtualMachineState
stateNotify *infinity.Channel[VirtualMachineState]
mu sync.RWMutex
}
// NewVirtualMachine creates a new VirtualMachine with VirtualMachineConfiguration.
//
// The configuration must be valid. Validation can be performed at runtime with (*VirtualMachineConfiguration).Validate() method.
// The configuration is copied by the initializer.
//
// This is only supported on macOS 11 and newer, error will
// be returned on older versions.
func NewVirtualMachine(config *VirtualMachineConfiguration) (*VirtualMachine, error) {
if err := macOSAvailable(11); err != nil {
return nil, err
}
// should not call Free function for this string.
cs := (*char)(objc.GetUUID())
dispatchQueue := C.makeDispatchQueue(cs.CString())
machineState := &machineState{
state: VirtualMachineState(0),
stateNotify: infinity.NewChannel[VirtualMachineState](),
}
stateHandle := cgo.NewHandle(machineState)
disconnectedIn := infinity.NewChannel[*disconnected]()
disconnectedOut := infinity.NewChannel[*DisconnectedError]()
disconnectedHandle := cgo.NewHandle(disconnectedIn)
v := &VirtualMachine{
id: cs.String(),
pointer: objc.NewPointer(
C.newVZVirtualMachineWithDispatchQueue(
objc.Ptr(config),
dispatchQueue,
C.uintptr_t(stateHandle),
C.uintptr_t(disconnectedHandle),
),
),
dispatchQueue: dispatchQueue,
machineState: machineState,
disconnectedIn: disconnectedIn,
disconnectedOut: disconnectedOut,
config: config,
}
objc.SetFinalizer(v, func(self *VirtualMachine) {
self.finalize()
stateHandle.Delete()
})
return v, nil
}
func (v *VirtualMachine) finalize() {
v.finalizeOnce.Do(func() {
objc.ReleaseDispatch(v.dispatchQueue)
objc.Release(v)
})
}
// SocketDevices return the list of socket devices configured on this virtual machine.
// Return an empty array if no socket device is configured.
//
// Since only NewVirtioSocketDeviceConfiguration is available in vz package,
// it will always return VirtioSocketDevice.
// see: https://developer.apple.com/documentation/virtualization/vzvirtualmachine/3656702-socketdevices?language=objc
func (v *VirtualMachine) SocketDevices() []*VirtioSocketDevice {
nsArray := objc.NewNSArray(
C.VZVirtualMachine_socketDevices(objc.Ptr(v)),
)
ptrs := nsArray.ToPointerSlice()
socketDevices := make([]*VirtioSocketDevice, len(ptrs))
for i, ptr := range ptrs {
socketDevices[i] = newVirtioSocketDevice(ptr, v.dispatchQueue)
}
return socketDevices
}
//export changeStateOnObserver
func changeStateOnObserver(newStateRaw C.int, cgoHandleUintptr C.uintptr_t) {
stateHandle := cgo.Handle(cgoHandleUintptr)
// I expected it will not cause panic.
// if caused panic, that's unexpected behavior.
v, _ := stateHandle.Value().(*machineState)
v.mu.Lock()
newState := VirtualMachineState(newStateRaw)
v.state = newState
v.stateNotify.In() <- newState
v.mu.Unlock()
}
// State represents execution state of the virtual machine.
func (v *VirtualMachine) State() VirtualMachineState {
v.machineState.mu.RLock()
defer v.machineState.mu.RUnlock()
return v.machineState.state
}
// StateChangedNotify gets notification is changed execution state of the virtual machine.
func (v *VirtualMachine) StateChangedNotify() <-chan VirtualMachineState {
v.machineState.mu.RLock()
defer v.machineState.mu.RUnlock()
return v.machineState.stateNotify.Out()
}
// CanStart returns true if the machine is in a state that can be started.
func (v *VirtualMachine) CanStart() bool {
return bool(C.vmCanStart(objc.Ptr(v), v.dispatchQueue))
}
// CanPause returns true if the machine is in a state that can be paused.
func (v *VirtualMachine) CanPause() bool {
return bool(C.vmCanPause(objc.Ptr(v), v.dispatchQueue))
}
// CanResume returns true if the machine is in a state that can be resumed.
func (v *VirtualMachine) CanResume() bool {
return (bool)(C.vmCanResume(objc.Ptr(v), v.dispatchQueue))
}
// CanRequestStop returns whether the machine is in a state where the guest can be asked to stop.
func (v *VirtualMachine) CanRequestStop() bool {
return (bool)(C.vmCanRequestStop(objc.Ptr(v), v.dispatchQueue))
}
// CanStop returns whether the machine is in a state that can be stopped.
//
// This is only supported on macOS 12 and newer, false will always be returned
// on older versions.
func (v *VirtualMachine) CanStop() bool {
if err := macOSAvailable(12); err != nil {
return false
}
return (bool)(C.vmCanStop(objc.Ptr(v), v.dispatchQueue))
}
//export virtualMachineCompletionHandler
func virtualMachineCompletionHandler(cgoHandleUintptr C.uintptr_t, errPtr unsafe.Pointer) {
cgoHandle := cgo.Handle(cgoHandleUintptr)
handler := cgoHandle.Value().(func(error))
if err := newNSError(errPtr); err != nil {
handler(err)
} else {
handler(nil)
}
}
func makeHandler() (func(error), chan error) {
ch := make(chan error, 1)
return func(err error) {
ch <- err
close(ch)
}, ch
}
type virtualMachineStartOptions struct {
macOSVirtualMachineStartOptionsPtr unsafe.Pointer
}
// VirtualMachineStartOption is an option for virtual machine start.
type VirtualMachineStartOption func(*virtualMachineStartOptions) error
// Start a virtual machine that is in either Stopped or Error state.
//
// If you want to listen status change events, use the "StateChangedNotify" method.
//
// If options are specified, also checks whether these options are
// available in use your macOS version available.
func (v *VirtualMachine) Start(opts ...VirtualMachineStartOption) error {
o := &virtualMachineStartOptions{}
for _, optFunc := range opts {
if err := optFunc(o); err != nil {
return err
}
}
h, errCh := makeHandler()
handle := cgo.NewHandle(h)
defer handle.Delete()
if o.macOSVirtualMachineStartOptionsPtr != nil {
C.startWithOptionsCompletionHandler(
objc.Ptr(v),
v.dispatchQueue,
o.macOSVirtualMachineStartOptionsPtr,
C.uintptr_t(handle),
)
} else {
C.startWithCompletionHandler(objc.Ptr(v), v.dispatchQueue, C.uintptr_t(handle))
}
return <-errCh
}
// Pause a virtual machine that is in Running state.
//
// If you want to listen status change events, use the "StateChangedNotify" method.
func (v *VirtualMachine) Pause() error {
h, errCh := makeHandler()
handle := cgo.NewHandle(h)
defer handle.Delete()
C.pauseWithCompletionHandler(objc.Ptr(v), v.dispatchQueue, C.uintptr_t(handle))
return <-errCh
}
// Resume a virtual machine that is in the Paused state.
//
// If you want to listen status change events, use the "StateChangedNotify" method.
func (v *VirtualMachine) Resume() error {
h, errCh := makeHandler()
handle := cgo.NewHandle(h)
defer handle.Delete()
C.resumeWithCompletionHandler(objc.Ptr(v), v.dispatchQueue, C.uintptr_t(handle))
return <-errCh
}
// RequestStop requests that the guest turns itself off.
//
// If returned error is not nil, assigned with the error if the request failed.
// Returns true if the request was made successfully.
func (v *VirtualMachine) RequestStop() (bool, error) {
nserrPtr := newNSErrorAsNil()
ret := (bool)(C.requestStopVirtualMachine(objc.Ptr(v), v.dispatchQueue, &nserrPtr))
if err := newNSError(nserrPtr); err != nil {
return ret, err
}
return ret, nil
}
// Stop stops a VM that’s in either a running or paused state.
//
// The completion handler returns an error object when the VM fails to stop,
// or nil if the stop was successful.
//
// If you want to listen status change events, use the "StateChangedNotify" method.
//
// Warning: This is a destructive operation. It stops the VM without
// giving the guest a chance to stop cleanly.
//
// This is only supported on macOS 12 and newer, error will be returned on older versions.
func (v *VirtualMachine) Stop() error {
if err := macOSAvailable(12); err != nil {
return err
}
h, errCh := makeHandler()
handle := cgo.NewHandle(h)
defer handle.Delete()
C.stopWithCompletionHandler(objc.Ptr(v), v.dispatchQueue, C.uintptr_t(handle))
return <-errCh
}
type startGraphicApplicationOptions struct {
title string
enableController bool
}
// StartGraphicApplicationOption is an option for display graphics start.
type StartGraphicApplicationOption func(*startGraphicApplicationOptions) error
// WithWindowTitle is an option to set window title of display graphics window.
func WithWindowTitle(title string) StartGraphicApplicationOption {
return func(sgao *startGraphicApplicationOptions) error {
sgao.title = title
return nil
}
}
// WithController is an option to set virtual machine controller on graphics window toolbar.
func WithController(enable bool) StartGraphicApplicationOption {
return func(sgao *startGraphicApplicationOptions) error {
sgao.enableController = enable
return nil
}
}
// StartGraphicApplication starts an application to display graphics of the VM.
//
// You must to call runtime.LockOSThread before calling this method.
//
// This is only supported on macOS 12 and newer, error will be returned on older versions.
func (v *VirtualMachine) StartGraphicApplication(width, height float64, opts ...StartGraphicApplicationOption) error {
if err := macOSAvailable(12); err != nil {
return err
}
defaultOpts := &startGraphicApplicationOptions{}
for _, opt := range opts {
if err := opt(defaultOpts); err != nil {
return err
}
}
windowTitle := charWithGoString(defaultOpts.title)
defer windowTitle.Free()
C.startVirtualMachineWindow(
objc.Ptr(v),
v.dispatchQueue,
C.double(width),
C.double(height),
windowTitle.CString(),
C.bool(defaultOpts.enableController),
)
return nil
}
// DisconnectedError represents an error that occurs when a VM’s network attachment is disconnected
// due to a network-related issue. This error is triggered by the framework when such a disconnection happens.
type DisconnectedError struct {
// Err is the underlying error that caused the disconnection, triggered by the framework.
// This error provides information on why the network attachment was disconnected.
Err error
// The network device configuration associated with the disconnection event.
// This configuration helps identify which network device experienced the disconnection.
// If Config is nil, the specific configuration details are unavailable.
Config *VirtioNetworkDeviceConfiguration
}
var _ error = (*DisconnectedError)(nil)
func (e *DisconnectedError) Unwrap() error { return e.Err }
func (e *DisconnectedError) Error() string {
if e.Config == nil {
return e.Err.Error()
}
return fmt.Sprintf("%s: %v", e.Config.attachment, e.Err)
}
type disconnected struct {
err error
index int
}
// NetworkDeviceAttachmentWasDisconnected returns a receive channel.
// The channel emits an error message each time the network attachment is disconnected,
// typically triggered by events such as failure to start, initial boot, device reset, or reboot.
// As a result, this method may be invoked multiple times throughout the virtual machine's lifecycle.
//
// This is only supported on macOS 12 and newer, error will be returned on older versions.
func (v *VirtualMachine) NetworkDeviceAttachmentWasDisconnected() (<-chan *DisconnectedError, error) {
if err := macOSAvailable(12); err != nil {
return nil, err
}
v.watchDisconnectedOnce.Do(func() {
go v.watchDisconnected()
})
return v.disconnectedOut.Out(), nil
}
// TODO(codehex): refactoring to leave using machineState's mutex lock.
func (v *VirtualMachine) watchDisconnected() {
for disconnected := range v.disconnectedIn.Out() {
v.mu.RLock()
config := sliceutil.FindValueByIndex(
v.config.networkDeviceConfiguration,
disconnected.index,
)
v.mu.RUnlock()
v.disconnectedOut.In() <- &DisconnectedError{
Err: disconnected.err,
Config: config,
}
}
v.disconnectedOut.Close()
}
//export emitAttachmentWasDisconnected
func emitAttachmentWasDisconnected(index C.int, errPtr unsafe.Pointer, cgoHandleUintptr C.uintptr_t) {
handler := cgo.Handle(cgoHandleUintptr)
err := newNSError(errPtr)
// I expected it will not cause panic.
// if caused panic, that's unexpected behavior.
ch, _ := handler.Value().(*infinity.Channel[*disconnected])
ch.In() <- &disconnected{
err: err,
index: int(index),
}
}
//export closeAttachmentWasDisconnectedChannel
func closeAttachmentWasDisconnectedChannel(cgoHandleUintptr C.uintptr_t) {
handler := cgo.Handle(cgoHandleUintptr)
// I expected it will not cause panic.
// if caused panic, that's unexpected behavior.
ch, _ := handler.Value().(*infinity.Channel[*disconnected])
ch.Close()
}