Skip to content

Conversation

@ben-grande
Copy link
Contributor

@ben-grande ben-grande commented Feb 21, 2025

For: QubesOS/qubes-issues#1512

How to test

Unit tests

./run-tests -l 2>&1 > /tmp/tests-unit
grep 'preload' /tmp/tests-unit  > /tmp/tests-unit-preload
./run-tests $(tr "\n" " " </tmp/tests-unit-preload)

Integration tests

cat <<EOF >~/run-tests
#!/bin/sh
systemctl stop qubesd
sudo -E python3 -m qubes.tests.run "\$@"
systemctl restart qubesd
EOF

chmod +x ~/run-tests
~/run-tests -l 2>&1 > /tmp/tests-integ
grep 'fedora.*preload' /tmp/tests-integ  > /tmp/tests-integ-preload
~/run-tests $(tr "\n" " " </tmp/tests-integ-preload)

:return:
"""
dispvm_template = self.arg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.dest?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But also, who will call it? If something from inside qubesd, then it doesn't need to be an "API" method, just a function somewhere, probably next to the dispvm class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.dest?

WIll fix.

But also, who will call it? If something from inside qubesd, then it doesn't need to be an "API" method, just a function somewhere, probably next to the dispvm class.

Haven't determined yet who will call it and not sure it will be something inside qubesd, all I gethered so far is that it needs to run after the autostarted qubes starts, which I presume can be easily monitored.

dispvm_template = self.arg
preload_dispvm = dispvm_template.features.get("preload-dispvm", None)
## TODO: should this function receive disposable name as argument
## or call admin.vm.CreateDisposable?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO makes more sense to create here.

There should be also somewhere a counter how many preloaded disposables should be and if that limit (which may be 0) is reached already, do nothing here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

:return:
"""
used_dispvm = self.arg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.dest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

Comment on lines 414 to 418
preload_dispvm = preload_dispvm.split(" ")
if use_first:
used_dispvm_name = preload_dispvm[1:]
else:
used_dispvm_name = used_dispvm.name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also check if the selected dispvm is actually one of preloaded ones.
But also, what is the use case of pointing at specific DispVM, instead of its template?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also check if the selected dispvm is actually one of preloaded ones.

Will fix.

But also, what is the use case of pointing at specific DispVM, instead of its template?

In case a specific preloaded dispvm is unpaused.

@qubes.api.method(
"internal.dispvm.preload.Use", scope="local", write=True
)
async def dispvm_preload_use(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be an utility function somewhere, not an API method exposed to external callers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also moved internal.dispvm.preload.Create as a utility function instead of API method.

Comment on lines 210 to 312
if self.is_domain_preloaded():
self.pause()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is easy thing to do, but requires testing if it isn't too early. Some services (especially gui-agent) may still be starting.
There is an qubes.WaitForSession service that may help (ensure to use async handler to not block qubesd while waiting on it).
And also, the event is called domain-start.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an qubes.WaitForSession service that may help (ensure to use async handler to not block qubesd while waiting on it).

What is preloaded dispvm has gui feature disabled in case the user is using the disposable to be qubes-builder-dvm for example, that doesn't require a GUI? What can be done in this case?

See also QubesOS/qubes-issues#9789 related to gui feature and +WaitForSession.

And also, the event is called domain-start.

Right... My mind was on domain-unpaused therefore domain-started.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is preloaded dispvm has gui feature disabled in case the user is using the disposable to be qubes-builder-dvm for example, that doesn't require a GUI? What can be done in this case?

Very good question. In that case I guess start even is all we have. Maybe with some (configurable?) delay?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a delay for both enabled gui and disabled gui. There is attribute/prefs qrexec_timeout, I used a delay lower than that, not sure it should be configurable as I don't know the benefits. The preloaded DispVM is paused when the timeout is reached.

def on_domain_unpaused(self):
"""Mark unpaused preloaded domains as used."""
if self.is_domain_preloaded():
self.app.qubesd_call(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't exist in core-admin - it's only in core-admin-client. Here, you are running inside qubesd already, you can simply call appropriate function directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

)
threshold = 1024 * 5
if memory >= (available_memory - threshold):
await DispVM.create_preloaded_dispvm(self)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong, should have called QubesAdminAPI.create_disposable with preload argument.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or rather DispVM.from_appvm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is tiny bit of code that I didn't want to duplicate from create_disposable():

        dispvm = await qubes.vm.dispvm.DispVM.from_appvm(
            dispvm_template, preload=preload
        )
        # TODO: move this to extension (in race-free fashion, better than here)
        dispvm.tags.add("created-by-" + str(self.src))
        dispvm.tags.add("disp-created-by-" + str(self.src))

The tags... although I see some tests not using it:

% rg from_appvm ../qubes-core-*
../qubes-core-admin/qubes/tests/vm/dispvm.py
90:    def test_000_from_appvm(self, mock_storage, mock_makedirs, mock_symlink):
104:                qubes.vm.dispvm.DispVM.from_appvm(self.appvm)
117:    def test_001_from_appvm_reject_not_allowed(self):
120:                qubes.vm.dispvm.DispVM.from_appvm(self.appvm)

../qubes-core-admin/qubes/tests/integ/dispvm.py
211:            qubes.vm.dispvm.DispVM.from_appvm(self.disp_base)
229:            qubes.vm.dispvm.DispVM.from_appvm(self.disp_base)

../qubes-core-admin/qubes/vm/dispvm.py
390:    async def from_appvm(cls, appvm, preload=False, **kwargs):
401:        >>> dispvm = qubes.vm.dispvm.DispVM.from_appvm(appvm).start()

../qubes-core-admin/qubes/api/admin.py
1291:        dispvm = await qubes.vm.dispvm.DispVM.from_appvm(

../qubes-core-admin-client/qubesadmin/tools/qvm_run.py
277:        dispvm = qubesadmin.vm.DispVM.from_appvm(args.app,

../qubes-core-admin-client/qubesadmin/tests/vm/dispvm.py
36:        vm = qubesadmin.vm.DispVM.from_appvm(self.app, None)
55:        vm = qubesadmin.vm.DispVM.from_appvm(self.app, 'test-vm')
66:        vm = qubesadmin.vm.DispVM.from_appvm(self.app, None)
72:        vm = qubesadmin.vm.DispVM.from_appvm(self.app, None)
82:        vm = qubesadmin.vm.DispVM.from_appvm(self.app, 'test-vm')
92:        vm = qubesadmin.vm.DispVM.from_appvm(self.app, None)

../qubes-core-admin-client/qubesadmin/vm/__init__.py
451:    def from_appvm(cls, app, appvm):

And you are correct, using create in the name of something that is not creating something, but marking, gave me the impression it would create the qube...

## before starting a qube?
memory = getattr(self, "memory", 0)
available_memory = (
psutil.virtual_memory().available / (1024 * 1024)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get what you mean, but this will not work. This looks only at free memory in dom0, not the whole system. And even if it would look more globally, qmemman tries to allocate available memory as much as possible. Only qmemman knows how much "free" memory you really have, and currently there is no API to query that...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will leave this for last, as it touches another component (qmmeman).

Copy link
Member

@marmarek marmarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the overall structure is getting close, but there are still several details. And tests missing, but this you know.

preload_dispvm_max = int(
self.features.check_with_template("preload-dispvm-max", 0)
)
if preload_dispvm_max == 0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is too late, no? I mean, the VM is created at this point already.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check probably should be in from_appvm when preload=True. And also similar check for >0, if there aren't enough preloaded dispvms already (and if there are, probably raise an exception, not just return).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is too late, no?

Yes, moved to from_appvm.

And also similar check for >0, if there aren't enough preloaded dispvms already

This case will be handled as an event instead of API method. On the next push, let's see what you think about it.

(and if there are, probably raise an exception, not just return).

Done.

dispvm_template = getattr(self, "template")
dispvm = self
elif (
self.klass == "AppVM"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is defined in a DispVM class, the klass is always DispVM here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing appvm clause.

self.klass == "AppVM"
and getattr(self, "template_for_dispvms", False)
):
dispvm_template = dispvm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides condition always being false, dispvm is not set anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by removing appvm clause.


preload_dispvm = dispvm_template.features.get("preload-dispvm", None)
if not preload_dispvm:
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks wrong - this shouldn't happen, as use_preloaded_dispvm should only be called on a preloaded dispvm, at this point there should be something in this feature. This should at least be a warning, if not even an exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed as everything that calls use_preloaded is checking if it is empty or even if a specific disposable is in it.

if dispvm.name not in preload_dispvm:
raise qubes.exc.QubesException("DispVM is not preloaded")
used_dispvm = dispvm.name
else:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment as above about the class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by removing clause.

dispvm.features["internal"] = False
## TODO: confirm if this guess of event code is correct
self.app.fire_event_for_permission(dispvm_template=dispvm_template)
dispvm_template.fire_event_async("domain-preloaded-dispvm-used")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_async needs await. And also, it may be useful to add dispvm (name? object?) as an event parameter.
And please document the event (there is specific sphinx syntax for events, see others for example).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

)
threshold = 1024 * 5
if memory >= (available_memory - threshold):
await DispVM.create_preloaded_dispvm(self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or rather DispVM.from_appvm

async def on_domain_unpaused(self):
"""Mark unpaused preloaded domains as used."""
if self.is_domain_preloaded():
await DispVM.use_preloaded_dispvm(self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.use_preloaded_dispvm()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

await dispvm.create_on_disk()

if preload:
await DispVM.create_preloaded_dispvm(dispvm)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dispvm.create_preloaded_dispvm()
But also, I think this function should be named like mark_preloaded() or such, as it doesn't really create anything, it's called when the VM is already created.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

if preload_dispvm:
dispvm = preload_dispvm.split(" ")[0]
dispvm = app.domains[dispvm]
await DispVM.use_preloaded_dispvm(dispvm)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dispvm.use_preloaded_dispvm()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@ben-grande ben-grande force-pushed the preload-dispvm branch 3 times, most recently from 596c882 to a76ed3e Compare February 27, 2025 18:11
@@ -0,0 +1,17 @@
[Unit]
Description=Preload Qubes DispVMs
After[email protected]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure it will work this way? I don't think it will cover all instances of the qubes-vm@ units...
But maybe adding Before=qubes-preload-dispvm.service to [email protected] file will work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed my option failed. I did test without rebooting a qube enabling on runtime, not a valid test.
I did not want to touch qubes-vm@ but ordering after a template seems undocumented. Tested the way you said and it is best.


[Service]
Type=oneshot
Environment=DISPLAY=:0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is display really needed here? Xorg may not be running yet at this stage (and even when it is, user may not be logged in yet).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied from [email protected], if it is not needed here, it is not needed there also. Maybe it is needed for the gui agent to be able to create the sys-net Network Manager tray icon for example

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested the following:

DISPLAY= qvm-start QUBE
qvm-run -- QUBE xterm

Also tested the service with

Environment=DISPLAY=
ExecStart=/usr/bin/qvm-start %i

And both worked.

It opened the terminal. It seems to not be needed. But in 2013 it was?

commit 59b9e43060c3b9bd717e9cc59979153f86f1b9b7 (tag: mm_59b9e430)
Author: Marek Marczykowski-Górecki <[email protected]>
Date:   Tue Nov 26 16:53:26 2013

    Fix VM autostart - set $DISPLAY env variable

    Without this, started qrexec-daemon would not have access to GUI,
    especially can't display Qubes RPC confirmation dialogs.

diff --git a/linux/systemd/[email protected] b/linux/systemd/[email protected]
index 1770d6d9..ffa61763 100644
--- a/linux/systemd/[email protected]
+++ b/linux/systemd/[email protected]
@@ -4,6 +4,7 @@ After=qubes-netvm.service

 [Service]
 Type=oneshot
+Environment=DISPLAY=:0
 ExecStart=/usr/bin/qvm-start --no-guid %i
 Group=qubes
 RemainAfterExit=yes

There is no option --no-guid anymore. For qvm_run.py, the default is to use args.gui if DISPLAY is set. I have found nothing special on qvm_start.py

Seems "safe" to remove from both services.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, things have changed since 2013, now qvm-start only sends an API call to qubesd, it doesn't start those services as its own children anymore.

b"admin.vm.CreateDisposable", b"dom0", arg="preload-autostart"
)
# TODO: doesn't return any value, so how to check if it was preloaded?
#dispvm_preload = self.vm.features.get("preload-dispvm", "").split(" ")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if there is (exactly) one entry?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) Yes... will fix.

Comment on lines 193 to 208
def get_feat_preload(self, feature):
if feature not in ["preload-dispvm", "preload-dispvm-max"]:
raise qubes.exc.QubesException("Invalid feature provided")

if feature == "preload-dispvm":
default = ""
elif feature == "preload-dispvm-max":
default = 0

value = self.features.check_with_template(feature, default)

if feature == "preload-dispvm":
return value.split(" ")
if feature == "preload-dispvm-max":
return int(value)
return None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to have two functions instead of the conditions in one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Is shorter, better.

@qubes.events.handler(
"domain-preloaded-dispvm-used", "domain-preloaded-dispvm-autostart"
)
async def on_domain_preloaded_dispvm_used(self, event, delay=5, **kwargs): # pylint: disable=unused-argument
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event is fired on disposable template, so it should be attached there, not to DispVM class. See DVMTemplateMixin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

:returns:
"""
await asyncio.sleep(delay)
while True:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a check for preload-dispvm-max early. Right now, it looks like it will preload always at least one dispvm even if preload-dispvm-max is 0. I know preload-dispvm has a check for that, but it's still open for races, plus admin.vm.CreateDisposable method can be called by others too (it's a public API, for those with appropriate permission).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines 282 to 286
# TODO: what to do if the maximum is never reached on autostart
# as there is not enough memory, and then a preloaded DispVM is
# used, calling for the creation of another one, while the
# autostart will also try to create one. Is this a race
# condition?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very good question. Yes, I think this race condition is there. Maybe wrap preloading dispvm in a lock? The section under lock should do all checks (max count, free memory) + actual creating. And when the max is reached (regardless if it's autostart or not), simply exit the loop. This way, even if two instances of this function runs in parallel, still correct number of dispvms will be preloaded, and both instances will finish eventually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, an async lock that exits when max is reached independent of the event seems like a great idea.

# W0236 (invalid-overridden-method) Method 'on_domain_started' was expected
# to be 'non-async', found it instead as 'async'
# TODO: Seems to conflict with qubes.vm.mix.net, which is pretty strange.
# Larger bug? qubes.vm.qubesvm.QubesVM has NetVMMixin... which conflicts...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to rename this function to not override one from the mixin...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


proc = None
try:
proc = await asyncio.wait_for(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run_service only starts the service. You need to wait for it to complete with proc.communicate() for example. Or simply use run_service_for_stdio which will do that for you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

if preload_dispvm:
dispvm = app.domains[preload_dispvm[0]]
await dispvm.use_preloaded()
return dispvm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unpause needs to happen at some point. Normally caller expect to receive not running VM, so it will call start() on it. But this won't unpause it... One option would be to modify start() to unpause VM if it's paused. Sounds like a big change, but maybe nothing will explode?
Alternatively, unpause here, and rely on the fact that start() on a running VM is no-op, so all should work (I hope).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both options seems to have different blast ranges. I will choose the smaller blast which is to unpause it here. Also in case it is not accompanied by .start(). Although I don't see any problem with unpausing when asking to start a qube, as running any Qrexec service targeting a qube already unpauses it.

@ben-grande ben-grande force-pushed the preload-dispvm branch 2 times, most recently from a1fe380 to e6cffff Compare March 13, 2025 21:15
[Unit]
Description=Start Qubes VM %i
After=qubesd.service qubes-meminfo-writer-dom0.service
Before[email protected]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Before=qubes-vm@.service
Before=qubes-preload-dispvm.service

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed....

self.app.domains[self.vm].fire_event = self.emitter.fire_event
self.vm.features["preload-dispvm-max"] = "1"
self.app.default_dispvm = self.vm
# TODO: how to mock start of a qube that we don't have its object?
Copy link
Member

@marmarek marmarek Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can also patch the start method at the class level, like unittest.mock.patch("qubes.vm.dispvm.DispVM.start"). A heavier hammer, but should be fine in this test context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did this. Thanks.

# on it).
# TODO:
# Test if pause isn't too late, what if application autostarts, will
# it open before the qube is paused?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will. Theoretically there is an "invisible" mode for gui-daemon for situation like this (it was used for very old implementation of DispVM that also kinda preloaded it). But there is no support for flipping it in runtime, gui-daemon needs to be restarted for that, so that's a broader change to use it in this version. Maybe later, I'd say it's okay to ignore this issue for now.

Copy link
Contributor Author

@ben-grande ben-grande Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this comment there. Nothing to do about this now.

@ben-grande ben-grande marked this pull request as ready for review March 14, 2025 19:18
@ben-grande
Copy link
Contributor Author

Removed draft but still not ready yet, but Gitlab CI fails to fetch amended and force pushed commits when using the Github API.

Having problems with events not being fired, trying to understand it.

@ben-grande ben-grande changed the title Preload disposables [WIP] Preload disposables Mar 14, 2025
]
method = "admin.vm.CreateDisposable"
for qube in appvms:
qube.qubesd_call("dom0", method, "preload-autostart")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks wrong, as the first argument is the call target. So, it calls the method on dom0 several times, instead of each disposable template...
See how qubesd_call is used in qubesadmin.vm.QubesVM. But since that uses private attribute, I guess it needs a wrapper (and then make all self.qubesd_call(self._method_dest, ...) use that new wrapper?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/QubesOS/qubes-core-admin-client/blob/main/qubesadmin/vm/__init__.py#L436-L441

            if self._method_dest.startswith('$dispvm:'):
                method_dest = self._method_dest[len('$dispvm:'):]
            else:
                method_dest = 'dom0'
            dispvm = self.app.qubesd_call(method_dest,
                'admin.vm.CreateDisposable')

I guess it can always be the disposable template in this case?

    for qube in appvms:
        qube.qubesd_call(qube.name, method, "preload-autostart")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the latter should work too.

@marmarek
Copy link
Member

This didn't went far:

Mar 19 17:31:15.017096 dom0 qubesd[4569]: unhandled exception while calling src=b'dom0' meth=b'admin.vm.Start' dest=b'sys-firewall' arg=b'' len(untrusted_payload)=0
Mar 19 17:31:15.017096 dom0 qubesd[4569]: Traceback (most recent call last):
Mar 19 17:31:15.017096 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/api/__init__.py", line 333, in respond
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     response = await self.mgmt.execute(
Mar 19 17:31:15.017096 dom0 qubesd[4569]:                ^^^^^^^^^^^^^^^^^^^^^^^^
Mar 19 17:31:15.017096 dom0 qubesd[4569]:         untrusted_payload=untrusted_payload
Mar 19 17:31:15.017096 dom0 qubesd[4569]:         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     )
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     ^
Mar 19 17:31:15.017096 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/api/admin.py", line 947, in vm_start
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     await self.dest.start()
Mar 19 17:31:15.017096 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 426, in start
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     await super().start(**kwargs)
Mar 19 17:31:15.017096 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 1522, in start
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     await self.fire_event_async(
Mar 19 17:31:15.017096 dom0 qubesd[4569]:         "domain-start", start_guid=start_guid
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     )
Mar 19 17:31:15.017096 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/events.py", line 234, in fire_event_async
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     sync_effects, async_effects = self._fire_event(
Mar 19 17:31:15.017096 dom0 qubesd[4569]:                                   ~~~~~~~~~~~~~~~~^
Mar 19 17:31:15.017096 dom0 qubesd[4569]:         event, kwargs, pre_event=pre_event
Mar 19 17:31:15.017096 dom0 qubesd[4569]:         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     )
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     ^
Mar 19 17:31:15.017096 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/events.py", line 169, in _fire_event
Mar 19 17:31:15.017096 dom0 qubesd[4569]:     effect = func(self, event, **kwargs)
Mar 19 17:31:15.017096 dom0 qubesd[4569]: TypeError: DispVM.on_domain_started_dispvm() got an unexpected keyword argument 'start_guid'

@marmarek
Copy link
Member

And this:

Mar 19 17:31:05.021673 dom0 qubesd[4569]: vm.sys-firewall: Activating the sys-firewall VM
Mar 19 17:31:05.040131 dom0 qubesd[4569]: Uncaught exception from domain-unpaused handler for domain sys-firewall
Mar 19 17:31:05.040131 dom0 qubesd[4569]: Traceback (most recent call last):
Mar 19 17:31:05.040131 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/app.py", line 1624, in _domain_event_callback
Mar 19 17:31:05.040131 dom0 qubesd[4569]:     vm.fire_event("domain-unpaused")
Mar 19 17:31:05.040131 dom0 qubesd[4569]:     ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
Mar 19 17:31:05.040131 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/events.py", line 200, in fire_event
Mar 19 17:31:05.040131 dom0 qubesd[4569]:     sync_effects, async_effects = self._fire_event(
Mar 19 17:31:05.040131 dom0 qubesd[4569]:                                   ~~~~~~~~~~~~~~~~^
Mar 19 17:31:05.040131 dom0 qubesd[4569]:         event, kwargs, pre_event=pre_event
Mar 19 17:31:05.040131 dom0 qubesd[4569]:         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Mar 19 17:31:05.040131 dom0 qubesd[4569]:     )
Mar 19 17:31:05.040131 dom0 qubesd[4569]:     ^
Mar 19 17:31:05.040131 dom0 qubesd[4569]:   File "/usr/lib/python3.13/site-packages/qubes/events.py", line 169, in _fire_event
Mar 19 17:31:05.040131 dom0 qubesd[4569]:     effect = func(self, event, **kwargs)
Mar 19 17:31:05.040131 dom0 qubesd[4569]: TypeError: DispVM.on_domain_unpaused() takes 1 positional argument but 2 were given

@marmarek
Copy link
Member

marmarek commented Jun 6, 2025

Uhm, looks like I forgot to include core-agent-linux PR in this test...

@ben-grande
Copy link
Contributor Author

ben-grande commented Jun 6, 2025

Not all qubes from the second batch loaded, they timed out after 120 seconds when trying to start the qube. The integration test logs don't tell much, but the journal says it was out of memory. It is easy to know that it is the second batch because of the video.

4 qubes trying to preload (I lowered from 5 because 8GB of RAM for this is too little at least before fixing the issue with pause with too much RAM) followed by 4 more qubes trying to preload. 3 qubes failed to start and 3 qmemman messages of failing to satisfy assignments.

Integration tests
qubes.tests.integ.dispvm/TC_20_DispVM_whonix-workstation-17/test_015_dvm_run_preload_race_more
Test race requesting multiple preloaded qubes ... CRITICAL:qubes.tests.integ.dispvm.TC_20_DispVM_whonix-workstation-17.test_015_dvm_run_preload_race_more:starting
�[0;31m�[0;31m�[0;31m�[0;31m�[0m�[0m�[0mERROR
ERROR:vm.disp2399:Start failed: Cannot connect to qrexec agent for 120 seconds, see /var/log/xen/console/guest-disp2399.log for details
VM disp2399 start failed at 2025-06-05 19:05:32
ERROR:vm.disp7862:Start failed: Cannot connect to qrexec agent for 120 seconds, see /var/log/xen/console/guest-disp7862.log for details
VM disp7862 start failed at 2025-06-05 19:05:32
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='Task-55523' coro=<Emitter.fire_event_async() done, defined at /usr/lib/python3.13/site-packages/qubes/events.py:211> exception=ExceptionGroup('unhandled errors in a TaskGroup', [QubesVMError('Cannot connect to qrexec agent for 120 seconds, see /var/log/xen/console/guest-disp7862.log for details')])>
  + Exception Group Traceback (most recent call last):
  |   File "/usr/lib/python3.13/site-packages/qubes/events.py", line 243, in fire_event_async
  |     effect = task.result()
  |   File "/usr/lib/python3.13/site-packages/qubes/vm/mix/dvmtemplate.py", line 283, in on_domain_preload_dispvm_used
  |     async with asyncio.TaskGroup() as task_group:
  |                ~~~~~~~~~~~~~~~~~^^
  |   File "/usr/lib64/python3.13/asyncio/taskgroups.py", line 71, in __aexit__
  |     return await self._aexit(et, exc)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/usr/lib64/python3.13/asyncio/taskgroups.py", line 173, in _aexit
  |     raise BaseExceptionGroup(
  |     ...<2 lines>...
  |     ) from None
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 2147, in start_qrexec_daemon
    |     await self.start_daemon(
    |     ...<4 lines>...
    |     )
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 2104, in start_daemon
    |     raise subprocess.CalledProcessError(
    |         p.returncode, command, output=stdout, stderr=stderr
    |     )
    | subprocess.CalledProcessError: Command '['runuser', '-u', 'user', '--', '/usr/sbin/qrexec-daemon', '-q', '-u', '2cf97795-c360-436b-ad24-caff898bc2db', '--', '67', 'disp7862', 'user']' returned non-zero exit status 3.
    | 
    | During handling of the above exception, another exception occurred:
    | 
    | Traceback (most recent call last):
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 529, in from_appvm
    |     await dispvm.start()
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 608, in start
    |     await super().start(**kwargs)
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 1532, in start
    |     await self.start_qrexec_daemon()
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 2155, in start_qrexec_daemon
    |     raise qubes.exc.QubesVMError(
    |     ...<5 lines>...
    |     )
    | qubes.exc.QubesVMError: Cannot connect to qrexec agent for 120 seconds, see /var/log/xen/console/guest-disp7862.log for details
    +------------------------------------
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='Task-55487' coro=<Emitter.fire_event_async() done, defined at /usr/lib/python3.13/site-packages/qubes/events.py:211> exception=ExceptionGroup('unhandled errors in a TaskGroup', [QubesVMError('Cannot connect to qrexec agent for 120 seconds, see /var/log/xen/console/guest-disp2399.log for details')])>
  + Exception Group Traceback (most recent call last):
  |   File "/usr/lib/python3.13/site-packages/qubes/events.py", line 243, in fire_event_async
  |     effect = task.result()
  |   File "/usr/lib/python3.13/site-packages/qubes/vm/mix/dvmtemplate.py", line 283, in on_domain_preload_dispvm_used
  |     async with asyncio.TaskGroup() as task_group:
  |                ~~~~~~~~~~~~~~~~~^^
  |   File "/usr/lib64/python3.13/asyncio/taskgroups.py", line 71, in __aexit__
  |     return await self._aexit(et, exc)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/usr/lib64/python3.13/asyncio/taskgroups.py", line 173, in _aexit
  |     raise BaseExceptionGroup(
  |     ...<2 lines>...
  |     ) from None
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 2147, in start_qrexec_daemon
    |     await self.start_daemon(
    |     ...<4 lines>...
    |     )
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 2104, in start_daemon
    |     raise subprocess.CalledProcessError(
    |         p.returncode, command, output=stdout, stderr=stderr
    |     )
    | subprocess.CalledProcessError: Command '['runuser', '-u', 'user', '--', '/usr/sbin/qrexec-daemon', '-q', '-u', '4fc2d0d5-5f24-4305-b995-a418df2762bd', '--', '66', 'disp2399', 'user']' returned non-zero exit status 3.
    | 
    | During handling of the above exception, another exception occurred:
    | 
    | Traceback (most recent call last):
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 529, in from_appvm
    |     await dispvm.start()
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 608, in start
    |     await super().start(**kwargs)
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 1532, in start
    |     await self.start_qrexec_daemon()
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 2155, in start_qrexec_daemon
    |     raise qubes.exc.QubesVMError(
    |     ...<5 lines>...
    |     )
    | qubes.exc.QubesVMError: Cannot connect to qrexec agent for 120 seconds, see /var/log/xen/console/guest-disp2399.log for details
    +------------------------------------
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='Task-58312' coro=<DispVM.on_domain_started_dispvm() done, defined at /usr/lib/python3.13/site-packages/qubes/vm/dispvm.py:332> exception=QubesException("Error on Qrexec call to 'qubes.WaitForSession' during preload startup")>
Traceback (most recent call last):
  File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 362, in on_domain_started_dispvm
    await asyncio.wait_for(
    ...<6 lines>...
    )
  File "/usr/lib64/python3.13/asyncio/tasks.py", line 507, in wait_for
    return await fut
           ^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 1888, in run_service_for_stdio
    raise subprocess.CalledProcessError(
        p.returncode, args[0], *stdouterr
    )
subprocess.CalledProcessError: Command 'qubes.WaitForSession' returned non-zero exit status 255.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 379, in on_domain_started_dispvm
    raise qubes.exc.QubesException(
        "Error on Qrexec call to '%s' during preload startup" % service
    )
qubes.exc.QubesException: Error on Qrexec call to 'qubes.WaitForSession' during preload startup
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='Task-58056' coro=<DispVM.on_domain_started_dispvm() done, defined at /usr/lib/python3.13/site-packages/qubes/vm/dispvm.py:332> exception=QubesException("Error on Qrexec call to 'qubes.WaitForSession' during preload startup")>
Traceback (most recent call last):
  File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 362, in on_domain_started_dispvm
    await asyncio.wait_for(
    ...<6 lines>...
    )
  File "/usr/lib64/python3.13/asyncio/tasks.py", line 507, in wait_for
    return await fut
           ^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 1888, in run_service_for_stdio
    raise subprocess.CalledProcessError(
        p.returncode, args[0], *stdouterr
    )
subprocess.CalledProcessError: Command 'qubes.WaitForSession' returned non-zero exit status 255.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 379, in on_domain_started_dispvm
    raise qubes.exc.QubesException(
        "Error on Qrexec call to '%s' during preload startup" % service
    )
qubes.exc.QubesException: Error on Qrexec call to 'qubes.WaitForSession' during preload startup
ERROR:vm.disp7639:Start failed: Timed out Qrexec call to 'qubes.WaitForSession' after '120' seconds during preload startup
VM disp7639 start failed at 2025-06-05 19:06:50
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='Task-54106' coro=<Emitter.fire_event_async() done, defined at /usr/lib/python3.13/site-packages/qubes/events.py:211> exception=ExceptionGroup('unhandled errors in a TaskGroup', [QubesException("Timed out Qrexec call to 'qubes.WaitForSession' after '120' seconds during preload startup")])>
  + Exception Group Traceback (most recent call last):
  |   File "/usr/lib/python3.13/site-packages/qubes/events.py", line 243, in fire_event_async
  |     effect = task.result()
  |   File "/usr/lib/python3.13/site-packages/qubes/vm/mix/dvmtemplate.py", line 283, in on_domain_preload_dispvm_used
  |     async with asyncio.TaskGroup() as task_group:
  |                ~~~~~~~~~~~~~~~~~^^
  |   File "/usr/lib64/python3.13/asyncio/taskgroups.py", line 71, in __aexit__
  |     return await self._aexit(et, exc)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/usr/lib64/python3.13/asyncio/taskgroups.py", line 173, in _aexit
  |     raise BaseExceptionGroup(
  |     ...<2 lines>...
  |     ) from None
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/usr/lib64/python3.13/asyncio/tasks.py", line 507, in wait_for
    |     return await fut
    |            ^^^^^^^^^
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 1885, in run_service_for_stdio
    |     stdouterr = await p.communicate(input=input)
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/usr/lib64/python3.13/asyncio/subprocess.py", line 202, in communicate
    |     await self.wait()
    |   File "/usr/lib64/python3.13/asyncio/subprocess.py", line 137, in wait
    |     return await self._transport._wait()
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/usr/lib64/python3.13/asyncio/base_subprocess.py", line 248, in _wait
    |     return await waiter
    |            ^^^^^^^^^^^^
    | asyncio.exceptions.CancelledError
    | 
    | The above exception was the direct cause of the following exception:
    | 
    | Traceback (most recent call last):
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 362, in on_domain_started_dispvm
    |     await asyncio.wait_for(
    |     ...<6 lines>...
    |     )
    |   File "/usr/lib64/python3.13/asyncio/tasks.py", line 506, in wait_for
    |     async with timeouts.timeout(timeout):
    |                ~~~~~~~~~~~~~~~~^^^^^^^^^
    |   File "/usr/lib64/python3.13/asyncio/timeouts.py", line 116, in __aexit__
    |     raise TimeoutError from exc_val
    | TimeoutError
    | 
    | During handling of the above exception, another exception occurred:
    | 
    | Traceback (most recent call last):
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 529, in from_appvm
    |     await dispvm.start()
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 608, in start
    |     await super().start(**kwargs)
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/qubesvm.py", line 1534, in start
    |     await self.fire_event_async(
    |         "domain-start", start_guid=start_guid
    |     )
    |   File "/usr/lib/python3.13/site-packages/qubes/events.py", line 243, in fire_event_async
    |     effect = task.result()
    |   File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 374, in on_domain_started_dispvm
    |     raise qubes.exc.QubesException(
    |     ...<2 lines>...
    |     )
    | qubes.exc.QubesException: Timed out Qrexec call to 'qubes.WaitForSession' after '120' seconds during preload startup
    +------------------------------------
WARNING:vm.disp7639:Requested preloaded qube but failed to finish preloading after '144' seconds, falling back to normal disposable
ERROR:asyncio:Task exception was never retrieved
future: <Task finished name='Task-59090' coro=<DispVM.cleanup() done, defined at /usr/lib/python3.13/site-packages/qubes/vm/dispvm.py:573> exception=AttributeError("'DispVM' object has no attribute 'app'")>
Traceback (most recent call last):
  File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 579, in cleanup
    if self not in self.app.domains:
                   ^^^^^^^^
AttributeError: 'DispVM' object has no attribute 'app'
ERROR:app:unhandled exception while calling src=b'dom0' meth=b'admin.vm.CreateDisposable' dest=b'test-inst-dvm' arg=b'' len(untrusted_payload)=0
Traceback (most recent call last):
  File "/usr/lib/python3.13/site-packages/qubes/api/__init__.py", line 333, in respond
    response = await self.mgmt.execute(
               ^^^^^^^^^^^^^^^^^^^^^^^^
        untrusted_payload=untrusted_payload
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/usr/lib/python3.13/site-packages/qubes/api/admin.py", line 1332, in create_disposable
    dispvm = await qubes.vm.dispvm.DispVM.from_appvm(appvm)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/site-packages/qubes/vm/dispvm.py", line 515, in from_appvm
    dispvm = app.add_new_vm(
        cls, template=appvm, auto_cleanup=True, **kwargs
    )
  File "/usr/lib/python3.13/site-packages/qubes/app.py", line 1416, in add_new_vm
    qid = self.domains.get_new_unused_qid()
          ^^^^^^^^^^^^
AttributeError: 'Qubes' object has no attribute 'domains'
FAIL
Journal

There are 3 of these logs

Jun 05 19:04:27.585433 dom0 qmemman.systemstate[1466]: Xen free = 356106234 too small to satisfy assignments! assigned_but_unused=355485297, domdict={'0': {'memory_current': 3972907008, 'memory_actual': 4294967296, 'memory_maximum': 4294967296, 'mem_used': 1433985024, 'id': '0', 'last_target': 4294967296, 'use_hotplug': False, 'no_progress': False, 'slow_memset_react': False}, '1': {'memory_current': 297840640, 'memory_actual': 297840640, 'memory_maximum': 314572800, 'mem_used': None, 'id': '1', 'last_target': 297795584, 'use_hotplug': False, 'no_progress': False, 'slow_memset_react': False}, '2': {'memory_current': 150994944, 'memory_actual': 150994944, 'memory_maximum': 150994944, 'mem_used': None, 'id': '2', 'last_target': 150994944, 'use_hotplug': False, 'no_progress': False, 'slow_memset_react': False}, '3': {'memory_current': 297840640, 'memory_actual': 297840640, 'memory_maximum': 314572800, 'mem_used': None, 'id': '3', 'last_target': 297795584, 'use_hotplug': False, 'no_progress': False, 'slow_memset_react': False}, '4': {'memory_current': 150994944, 'memory_actual': 150994944, 'memory_maximum': 150994944, 'mem_used': None, 'id': '4', 'last_target': 150994944, 'use_hotplug': False, 'no_progress': False, 'slow_memset_react': False}, '5': {'memory_current': 985567232, 'memory_actual': 1002279084, 'memory_maximum': 4194304000, 'mem_used': 355131392, 'id': '5', 'last_target': 1002279084, 'use_hotplug': True, 'no_progress': False, 'slow_memset_react': False}, '6': {'memory_current': 1237733376, 'memory_actual': 1254446533, 'memory_maximum': 4194304000, 'mem_used': 451739648, 'id': '6', 'last_target': 1254446533, 'use_hotplug': True, 'no_progress': False, 'slow_memset_react': False}, '60': {'memory_current': 419495936, 'memory_actual': 419495936, 'memory_maximum': 4194304000, 'mem_used': None, 'id': '60', 'last_target': 419430400, 'use_hotplug': True, 'no_progress': False, 'slow_memset_react': False}, '61': {'memory_current': 419495936, 'memory_actual': 419495936, 'memory_maximum': 4194304000, 'mem_used': None, 'id': '61', 'last_target': 419430400, 'use_hotplug': True, 'no_progress': False, 'slow_memset_react': False}}


In other words, what can be done to fix this? Make Whonix-Workstation preload less qubes (2 or 3) as it is too resource intensive?

@ben-grande
Copy link
Contributor Author

Uhm, looks like I forgot to include core-agent-linux PR in this test...

Organized the main issue QubesOS/qubes-issues#1512, use PR's from the MVP section.

@ben-grande ben-grande force-pushed the preload-dispvm branch 3 times, most recently from 885a894 to 4edf004 Compare June 6, 2025 18:33
Is a disposable state, it runs on the background and awaits to intercept
calls made to disposables of the same disposable template, having faster
execution times. Information of the design choices is present in the
DispVM class.

To use them, set the number of allowed preloaded for each disposable
template with the feature "preload-dispvm-max". In case there is not
enough available memory, the maximum won't be preloaded at this
instance, but will retry later on at any relevant event, such as a
preloaded being used or a normal qube being requested while a preload
can be created.

GUI daemon requires a running qube to connect to the GUI agent in the
qube, because of that, auto starting the preload mechanism only happens
after a GUI login.

Preloaded qubes are hidden from GUI applications until the qube itself
is requested to be used. Any GUI application that allows opening
applications on preloaded qubes before they are marked as used is
considered a bug. Applications that autostart on the qube are shown
before the qube has its state interrupted, it is also a bug but that
can't be fixed easily.

For: QubesOS/qubes-issues#1512
For: QubesOS/qubes-issues#9918
@marmarek marmarek merged commit 78aacd1 into QubesOS:main Jun 7, 2025
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants