@@ -151,9 +151,33 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
151
151
152
152
*other arguments are as in :py:meth:`start`*
153
153
154
+ .. event:: domain-stopped (subject, event)
155
+
156
+ Fired when domain has been stopped.
157
+
158
+ This event is emitted before ``'domain-shutdown'`` and will trigger
159
+ the cleanup in QubesVM. So if you require that the cleanup has
160
+ already run use ``'domain-shutdown'``.
161
+
162
+ Note that you can receive this event as soon as you received
163
+ ``'domain-pre-start'``. This also can be emitted in case of a
164
+ startup failure, before or after ``'domain-start-failed'``.
165
+
166
+ Handler for this event can be asynchronous (a coroutine).
167
+
168
+ :param subject: Event emitter (the qube object)
169
+ :param event: Event name (``'domain-stopped'``)
170
+
154
171
.. event:: domain-shutdown (subject, event)
155
172
156
- Fired when domain has been shut down.
173
+ Fired when domain has been shut down. It is generated after
174
+ ``'domain-stopped'``.
175
+
176
+ Note that you can receive this event as soon as you received
177
+ ``'domain-pre-start'``. This also can be emitted in case of a
178
+ startup failure, before or after ``'domain-start-failed'``.
179
+
180
+ Handler for this event can be asynchronous (a coroutine).
157
181
158
182
:param subject: Event emitter (the qube object)
159
183
:param event: Event name (``'domain-shutdown'``)
@@ -643,6 +667,17 @@ def __init__(self, app, xml, volume_config=None, **kwargs):
643
667
self ._libvirt_domain = None
644
668
self ._qdb_connection = None
645
669
670
+ # We assume a fully halted VM here. The 'domain-init' handler will
671
+ # check if the VM is already running.
672
+ self ._domain_stopped_event_received = True
673
+ self ._domain_stopped_event_handled = True
674
+
675
+ self ._domain_stopped_future = None
676
+
677
+ # Internal lock to ensure ordering between _domain_stopped_coro() and
678
+ # start(). This should not be accessed anywhere else.
679
+ self ._domain_stopped_lock = asyncio .Lock ()
680
+
646
681
if xml is None :
647
682
# we are creating new VM and attributes came through kwargs
648
683
assert hasattr (self , 'qid' )
@@ -712,6 +747,8 @@ def on_domain_init_loaded(self, event):
712
747
713
748
if not self .app .vmm .offline_mode and self .is_running ():
714
749
self .start_qdb_watch ()
750
+ self ._domain_stopped_event_received = False
751
+ self ._domain_stopped_event_handled = False
715
752
716
753
@qubes .events .handler ('property-set:label' )
717
754
def on_property_set_label (self , event , name , newvalue , oldvalue = None ):
@@ -797,6 +834,30 @@ def start(self, start_guid=True, notify_function=None,
797
834
if self .get_power_state () != 'Halted' :
798
835
return
799
836
837
+ with (yield from self ._domain_stopped_lock ):
838
+ # Don't accept any new stopped event's till a new VM has been
839
+ # created. If we didn't received any stopped event or it wasn't
840
+ # handled yet we will handle this in the next lines.
841
+ self ._domain_stopped_event_received = True
842
+
843
+ if self ._domain_stopped_future is not None :
844
+ # Libvirt stopped event was already received, so cancel the
845
+ # future. If it didn't generate the Qubes events yet we
846
+ # will do it below.
847
+ self ._domain_stopped_future .cancel ()
848
+ self ._domain_stopped_future = None
849
+
850
+ if not self ._domain_stopped_event_handled :
851
+ # No Qubes domain-stopped events have been generated yet.
852
+ # So do this now.
853
+
854
+ # Set this immediately such that we don't generate events
855
+ # twice if an exception gets thrown.
856
+ self ._domain_stopped_event_handled = True
857
+
858
+ yield from self .fire_event_async ('domain-stopped' )
859
+ yield from self .fire_event_async ('domain-shutdown' )
860
+
800
861
self .log .info ('Starting {}' .format (self .name ))
801
862
802
863
yield from self .fire_event_async ('domain-pre-start' ,
@@ -816,14 +877,17 @@ def start(self, start_guid=True, notify_function=None,
816
877
qmemman_client = yield from asyncio .get_event_loop ().\
817
878
run_in_executor (None , self .request_memory , mem_required )
818
879
880
+ yield from self .storage .start ()
881
+
819
882
except Exception as exc :
820
883
# let anyone receiving domain-pre-start know that startup failed
821
884
yield from self .fire_event_async ('domain-start-failed' ,
822
885
reason = str (exc ))
886
+ if qmemman_client :
887
+ qmemman_client .close ()
823
888
raise
824
889
825
890
try :
826
- yield from self .storage .start ()
827
891
self ._update_libvirt_domain ()
828
892
829
893
self .libvirt_domain .createWithFlags (
@@ -833,12 +897,16 @@ def start(self, start_guid=True, notify_function=None,
833
897
# let anyone receiving domain-pre-start know that startup failed
834
898
yield from self .fire_event_async ('domain-start-failed' ,
835
899
reason = str (exc ))
900
+ self .storage .stop ()
836
901
raise
837
902
838
903
finally :
839
904
if qmemman_client :
840
905
qmemman_client .close ()
841
906
907
+ self ._domain_stopped_event_received = False
908
+ self ._domain_stopped_event_handled = False
909
+
842
910
try :
843
911
yield from self .fire_event_async ('domain-spawn' ,
844
912
start_guid = start_guid )
@@ -870,24 +938,50 @@ def start(self, start_guid=True, notify_function=None,
870
938
871
939
return self
872
940
873
- @asyncio .coroutine
874
- def on_domain_shutdown_coro (self ):
875
- '''Coroutine for executing cleanup after domain shutdown.
876
- Do not allow domain to be started again until this finishes.
941
+ def on_libvirt_domain_stopped (self ):
942
+ ''' Handle VIR_DOMAIN_EVENT_STOPPED events from libvirt.
943
+
944
+ This is not a Qubes event handler. Instead we do some sanity checks
945
+ and synchronization with start() and then emits Qubes events.
877
946
'''
878
- with (yield from self .startup_lock ):
879
- try :
880
- yield from self .storage .stop ()
881
- except qubes .storage .StoragePoolException :
882
- self .log .exception ('Failed to stop storage for domain %s' ,
883
- self .name )
884
-
885
- @qubes .events .handler ('domain-shutdown' )
886
- def on_domain_shutdown (self , _event , ** _kwargs ):
887
- '''Cleanup after domain shutdown'''
888
- # TODO: ensure that domain haven't been started _before_ this
889
- # coroutine got a chance to acquire a lock
890
- asyncio .ensure_future (self .on_domain_shutdown_coro ())
947
+
948
+ state = self .get_power_state ()
949
+ if state not in ['Halted' , 'Crashed' , 'Dying' ]:
950
+ self .log .warning ('Stopped event from libvirt received,'
951
+ ' but domain is in state {}!' .format (state ))
952
+ # ignore this unexpected event
953
+ return
954
+
955
+ if self ._domain_stopped_event_received :
956
+ self .log .warning ('Duplicated stopped event from libvirt received!' )
957
+ # ignore this unexpected event
958
+ return
959
+
960
+ self ._domain_stopped_event_received = True
961
+ self ._domain_stopped_future = \
962
+ asyncio .ensure_future (self ._domain_stopped_coro ())
963
+
964
+ @asyncio .coroutine
965
+ def _domain_stopped_coro (self ):
966
+ with (yield from self ._domain_stopped_lock ):
967
+ assert not self ._domain_stopped_event_handled
968
+
969
+ # Set this immediately such that we don't generate events twice if
970
+ # an exception gets thrown.
971
+ self ._domain_stopped_event_handled = True
972
+
973
+ yield from self .fire_event_async ('domain-stopped' )
974
+ yield from self .fire_event_async ('domain-shutdown' )
975
+
976
+ @qubes .events .handler ('domain-stopped' )
977
+ @asyncio .coroutine
978
+ def on_domain_stopped (self , _event , ** _kwargs ):
979
+ '''Cleanup after domain was stopped'''
980
+ try :
981
+ yield from self .storage .stop ()
982
+ except qubes .storage .StoragePoolException :
983
+ self .log .exception ('Failed to stop storage for domain %s' ,
984
+ self .name )
891
985
892
986
@asyncio .coroutine
893
987
def shutdown (self , force = False , wait = False ):
0 commit comments