Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handling of graphical user services and ordering #253

Open
q66 opened this issue Nov 1, 2023 · 8 comments
Open

handling of graphical user services and ordering #253

q66 opened this issue Nov 1, 2023 · 8 comments

Comments

@q66
Copy link
Contributor

q66 commented Nov 1, 2023

I'm currently thinking about what the best way to implement graphical user services would be.

Ideally I'd like to have a special user service (called e.g. graphical.target). This user service would be the sentinel marking when graphical services are allowed to start; depending on this service would ensure that enabled graphical services will not attempt startup until a graphical environment is up (and thus it is ensured that e.g. DISPLAY and WAYLAND_DISPLAY are in environment, etc)

However, the implementation of the details of this is pretty complicated. Currently the only way dinit provides to block off a service from starting is using triggered services. I could make graphical.target a triggered service and that will block off graphical services from bringing it up until something lets it. However, using a trigger to permit statup does not solve the other part of the matter and that is shutdown.

The graphical.target should go down when the graphical session shuts down, and it should take all its dependents with it. There are some possible approaches, all of them with problems. The most obvious one is that graphical.target could be a service that is enabled by the user and begins activation as soon as user dinit comes up, then the trigger could be done from some kind of session script. Then to bring it down, some kind of shutdown script would untrigger it and forcibly bring it down. While this would probably typically work, it's not very robust as it provides zero guarantee that the graphical session won't die in the meantime without properly taking it down. (scoping it from the outside somehow would not help because it needs to be scoped on the inside, i.e. start after the environment is up and stop before the environment comes down)

Ideally the pieces providing the graphical session would also be user services themselves; they could in theory trigger bringup of the graphical.target somehow. However, dinit has no way to represent this kind of thing at the moment.

Thoughts?

@davmac314
Copy link
Owner

davmac314 commented Nov 1, 2023

Currently the only way dinit provides to block off a service from starting is using triggered services.

I guess what you're asking for is for the graphical target to be a non-process-based service, so yes, it has to be either triggered or have a dependency on another (process-based) service.

I'm also assuming you want to be able to switch out window systems implementations / desktop environments and therefore don't want to just hardcode a dependency from the graphical.target to a particular system. In that case, I think a combination of trigger and a transient, dynamically-added dependency is at least a step towards a solution? I.e.

  1. X session is going to start
  2. dinitctl add-dep regular graphical.target X (i.e. add the transient dependency from graphical.target to X)
  3. X starts and does: dinitctl trigger graphical.target (optionally, the trigger can be done immediately after adding the transient dependency; at that point graphical.target has to wait for X anyway)
  4. graphical.target will reach STARTED state.

Replace X in the above with the appropriate desktop session if you want to wait for it to start fully, which is probably better.

Then you need:

  • ideally, the trigger to reset once graphical.target starts
  • ideally, transient dependency to be removed as soon as X stops

Neither of those two latter can be done automatically right now, but it'd be pretty easy to add these features. Do you think that would work?

@q66
Copy link
Contributor Author

q66 commented Nov 1, 2023

hm, i guess that could work - i don't see a good way to remove the transient dependency though (it has to be there during the stop process to bring the services down) - but perhaps it can stay there until the next time it's retriggered?

hm i guess not really because dependencies will start regardless of trigger status, so it really has to go - the target would have to reset its dependencies immediately after its stop for this to work

@q66
Copy link
Contributor Author

q66 commented Nov 1, 2023

also i think I'd probably still need a way to implement service lifetime that is bounded by something external - i could make the graphical session a service, but something still needs to start it, the implementation of which is probably going to be specific to each desktop...

@davmac314
Copy link
Owner

hm, i guess that could work - i don't see a good way to remove the transient dependency though (it has to be there during the stop process to bring the services down) - but perhaps it can stay there until the next time it's retriggered?

I'm thinking about adding this support directly in dinit, so that when you add the transient dependency via dinitctl, you specify (with some option) that the dependency link should be automatically removed (when the dependent stops).

also i think I'd probably still need a way to implement service lifetime that is bounded by something external

I think the transient dependency handles this, since the graphical target depends on the desktop manager (or whatever) then it must stop if the manager stops. I.e. a service B which depends (with a hard dependency) on A is bound by A. Or am I not understanding you properly?

@q66
Copy link
Contributor Author

q66 commented Nov 2, 2023

well, i guess we could implement the whole main program (compositor or window manager for simple desktops, for complex desktops this is a bit more complicated, i think e.g. for gnome we'd need gnome-session) of the graphical session as a service - this wouldn't guarantee orderly shutdown though, since the program exiting will only trigger shutdown of graphical services after that, but perhaps that could be only a fallback (in case of e.g. a crash) and user-intended shutdown could be done by asking dinit to take down the service? (which would if i understand this correctly order things right - as it'd request stop of dependent services first?)

implementing that can be tricky however - since i have my user dinit running separately from the login, it does not have access to the user console, environment, etc.

  1. for X11 window managers, the WM probably does not really need to, as the X11 server handles everything that's necessary for that, and the window manager probably only really needs DISPLAY, which is already exported in the user activation environment at the time; however, the .xinitrc or whatever method is used to launch the WM still needs to wait until the WM terminates, to keep the X11 server active
  2. for Wayland environments, the compositor needs to be able to modeset - for logind environments, this means exporting at least the XDG_SESSION_ID variable from the console the compositor is to be started on, for seatd i'm not sure (i think seatd might always just preserve the currently active VT and not care further, i have to test that later)

Besides that, in both cases, users generally expect their graphical environment to have started with the environment of the place they invoked it from. That probably means exporting all of the environment variables from the current console (or whatever) to the startup environment of the graphical env. Moreover, things like resource limits and so on should likely also be inherited...

@davmac314
Copy link
Owner

but perhaps that could be only a fallback (in case of e.g. a crash) and user-intended shutdown could be done by asking dinit to take down the service?

Exactly what I was thinking.

this wouldn't guarantee orderly shutdown though, since the program exiting will only trigger shutdown of graphical services after that

Right, if there is an "outside of dinit" way to shutdown a process (service) then dinit can't enforce ordering. I don't think there's any way around that; what should happen in this case is that the session, before it ends, needs to stop the graphical.target service, which will stop its dependents. That has to be action on the part of the session itself, since dinit can't intercept it. What dinit can ensure is that if the session crashes (or for whatever reason exits without bringing down graphical.target first), that graphical.target (and its dependents) then also stop.

implementing that can be tricky however - since i have my user dinit running separately from the login, it does not have access to the user console, environment, etc.

Besides that, in both cases, users generally expect their graphical environment to have started with the environment of the place they invoked it from.

Right, these are the issues that come from implementing the session as a service. I think I understand what you are getting at.

If the session isn't a service though, you need some way for dinit to automatically know when a non-service process terminates.

One alternative: If you had the ability to attach an already-running process (the session manager) to a service (graphical.target) such that if the process terminates, the service must be stopped, I guess that would help? Then the session manager doesn't need to run as a service itself. This could probably be done (although it would be platform dependent, it's not normally possible to monitor a non-descendant process) though would be inherently racy, just as with anything involving non-descendant PIDs.

Alternatively, if you could have the session manager open a pipe and pass the read end to dinit (this would require implementation) and associating that with graphical.target, dinit could detect the process termination by monitoring the pipe (the write end will be closed automatically when the process terminates, assuming it doesn't also pass it on to child processes). This would be the most portable solution and is race-free but maybe more intrusive in terms of what changes are required to be implemented by the session instance itself.

The easiest solution for now is probably to have the session instance be a service, though you have to copy the environment to it when it is launched. Or just live with the possibility that the session crashes without graphical.target being brought down automatically, at least until a better solution is available.

@q66
Copy link
Contributor Author

q66 commented Nov 2, 2023

I was thinking of perhaps two pieces of functionality:

  1. An option to dinitctl start that would serialize the current environment and rlimits and use that for startup of the service in place of the ordinary activation environment (or perhaps merged with the regular activation environment? not sure)
  2. A command like dinit-run which would run a process on foreground and create a transient process service (with a name the user chooses, or with a random temporary unique name that would be communicated to the user maybe via env var, perhaps both) that would exist for the lifetime of the process; command line switches could exist to set the usual fields for that transient service (readiness notification, dependencies, whether to pass dinitctl fd etc) - one could further specify dynamic dependencies and so on from the inside - there could be two modes, one where the process is launched and supervised by dinit (and dinit-run merely waits for it to go away), one where the process is launched by dinit-run (in which case it'd be the other way around) - this would also have alternative uses, such as pinning a regular service in place temporarily by creating a virtual service depending on it with a temporary lifetime

the first one would allow implementation as a service, the second one would allow a compatible implementation that could be launched tranditionally

your pipe idea would be worthwhile to have too, independently of this issue, as it would nicely cover other things

i dunno about the process attachment one, i think it's better to prefer equivalent portable options when we can have them

as for manually copying the environment, i don't like that because it reflects that in activation env of every subsequently launched service, which I'm not a fan of

@q66
Copy link
Contributor Author

q66 commented Oct 3, 2024

i more or less came up with a system that i can probably implement without extra stuff in dinit (but it will need extra stuff elsewhere...)

  • the boot user service:
    • the service that is started upon login
    • waits-for.d = boot.d
    • depends-on = graphical.target
  • the login.target service:
    • internal, no dependencies
  • the graphical.target service:
    • triggered
    • depends-on = login.target

now other services:

  • any service that does not need graphical stuff and needs to start before login succeeds:
    • has before = login.target
  • any service that does not need graphical stuff and does not need to start before login succeeds:
    • does not need to have any target deps
    • if it must start after login, depends-on = login.target
  • any service that must start before graphical stuff comes up can set before = graphical.target
  • any service that is graphical must set depends-on = graphical.target

now, the user service manager (turnstile) starts dinit upon login, but does not wait for boot to come up; instead, it only waits for login.target before notifying login success

now for the graphical triggering:

  1. turnstile will grow logind-style functionality of tracking session types; i.e. it will know when a session has become graphical - this will be done by it getting seat functionality (i.e. getting GPU device file descriptors on the compositor's behalf, like logind or seatd, plus a backend for libseat which will let it support any desktop that already uses libseat, and perhaps some temporary integration with elogind while elogind is still present, which already has this functionality too) - since turnstile will know about the session type changes, it will be able to auto-trigger the graphical target (after automatically exporting the display and so on from the requesting compositor's env) upon the session gaining graphical type (x11 or wayland) to let the graphical services finish starting, and upon the session type becoming a console or seatless again, it can forcibly stop graphical.target (which will take all the graphical services down as they depend on the target)
  2. the desktop sessions will be expected to also attempt to attempt graphical.target stop cleanly upon the logout; the session type-based shutdown will be a fallback only

this requires no transient dependency to deal with, and should provide reasonable enough behavior in a desktop-independent manner

i will still have turnstile wait for boot to fully finish, at which point it will clear the trigger on the graphical target (so it can repeat next time)

that means turnstile service backend will become somewhat more complex; right now it only performs startup and waits for boot to fully become available at which point it lets login through; after this it will have to keep track of additional states (particularly "login succeeded but still waiting for some stuff to finish" and "boot service is fully up") but should not be a big deal

a nice thing about this approach is that i can already implement some of the baseline requirements without needing the session type tracking - i.e. the graphical services and whatnot, it'll just require users to for the time being manually deal with exporting the vars + triggering graphical.target themselves from some kind of session script + shutting down graphical.target on logout from some other place (and of course, crashed desktops will not take down the graphical.target, but that's okay for now...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants