diff options
-rw-r--r-- | doc/README.invoke-rc.d | 135 | ||||
-rw-r--r-- | doc/README.policy-rc.d | 102 | ||||
-rw-r--r-- | man8/invoke-rc.d.rst | 223 | ||||
-rw-r--r-- | man8/service.rst | 88 | ||||
-rw-r--r-- | man8/update-rc.d.rst | 249 | ||||
-rwxr-xr-x | script/deb-systemd-helper | 691 | ||||
-rwxr-xr-x | script/deb-systemd-invoke | 187 | ||||
-rwxr-xr-x | script/invoke-rc.d | 574 | ||||
-rwxr-xr-x | script/service | 217 | ||||
-rwxr-xr-x | script/update-rc.d | 543 | ||||
-rw-r--r-- | t/001-deb-systemd-helper.t | 473 | ||||
-rw-r--r-- | t/002-deb-systemd-helper-update.t | 196 | ||||
-rw-r--r-- | t/003-deb-systemd-helper-complex.t | 123 | ||||
-rw-r--r-- | t/004-deb-systemd-helper-user.t | 413 | ||||
-rw-r--r-- | t/README | 15 | ||||
-rw-r--r-- | t/helpers.pm | 160 |
16 files changed, 4389 insertions, 0 deletions
diff --git a/doc/README.invoke-rc.d b/doc/README.invoke-rc.d new file mode 100644 index 0000000..473fd2d --- /dev/null +++ b/doc/README.invoke-rc.d @@ -0,0 +1,135 @@ + + +This is the internal documentation for invoke-rc.d, as +written by Henrique M Holschuh <hmh@debian.org> + +This document can be found on the web as well at +http://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt + +There is also the Debian BTS entry for the invoke-rc.d policy change at +http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=76868 + + +INVOKE-RC.D (/usr/sbin/invoke-rc.d) interface: +============================================== + +The interface for all implementations of invoke-rc.d is mandated by the base +implementation in the sysvinit package, just like it is done for +update-rc.d. + +There is a provision for a "local initscript policy layer" (read: a call to +/usr/sbin/policy-rc.d if this executable is present in the local system), +which allows the local system administrator to control the behaviour of +invoke-rc.d for every initscript id and action. It is assumed that this +script is OPTIONAL and will by written and provided by packages other than +the initscript system (sysvinit and file-rc packages). + +The basic interface for all implementations of policy-rc.d is mandated by +the requirements of the base implementation of invoke-rc.d. This interface +will be described either in the manpage of invoke-rc.d, and in a text file +stored in /usr/share/doc/sysvinit/ by package sysvinit (which will host the +base implementation of invoke-rc.d). + +Proposed script interfaces: + +invoke-rc.d [options] <basename> <action> [extra initscript parameters...] + + basename - Initscript ID, as per update-rc.d(8) + action - Initscript action. Known actions are: + start, [force-]stop, [try-]restart, + [force-]reload, status + (status is there because of the LSB. Debian does not use it). + + extra initscript parameters: These parameters are passed to the initscript + as is, after the action parameter. <action> is always the first paramenter + to the initscript, and may be modified by fallback actions or policy-rc.d + requests. Note, however, that the extra parameters are not dropped or + modified even if the action (first parameter) is modified. + +Options: + + --quiet + Quiet mode, no error messages are generated by invoke-rc.d; policy-rc.d + is also called with --quiet if this option is in effect. + + --force + Try to run init script regardless of policy and non-fatal errors. Use + of this option in automated scripts is severely discouraged as it + bypasses integrity checks. If the initscript cannot be executed, error + status 102 is returned. Do note that the policy layer call + (policy-rc.d) is NOT skipped, although its results are ignored. + + --try-anyway + Try to run the initscript even if a non-fatal subsystem error is + detected (e.g: bad rc.d symlinks). A 102 status exit code will result + if init script fails to execute anyway). Unlike --force, policy is + still enforced with --try-anyway. + + --disclose-deny + Return status code 101 instead of status code 0 if initscript action is + denied by local policy rules or runlevel constrains. An warning is + generated if the action is denied. + + --query + Returns one of status codes 100-106, does not execute the init.d + script. Implies --disclose-deny and --nofallback. Status codes 104-106 + are only generated by this option. + + Note many messages are still sent to stderr in --query mode, including + those regarding policy overrides and subsystem errors. Use --quiet if + silent --query operation is desired. + + --no-fallback + The policy layer (policy-rc.d) may return fallback actions to be run + instead of the requested action. If this option is active, a fallback + action request will be ignored and a "action not allowed" reply used in + its place. This is probably a BAD idea unless you know exactly what + you're doing. + + --help + Outputs help message to stdout + +Unknown actions may generate warnings, but are passed to the underlying +initscript anyway. The reason for the warning is simple: It is very unlikely +that an unknown action (by invoke-rc.d) will be known to the policy layer +(policy-rc.d), and therefore it may cause an initscript to execute an action +which the local system administrator would have not allowed had he known +about it. If policy-rc.d is not present, no warnings for unknown actions +are generated. + +Should an initscript be executed, invoke-rc.d ALWAYS returns the status code +returned by the initscript. Initscripts should not return status codes in +the 100+ range (this is also a LSB requirement). + +Exit status codes (LSB compatible): + 0 : success + either the init script was run and returned exit status 0 (note + that a fallback action may have been run instead of the one given + in the command line), or it was not run because of runlevel/local + policy constrains and --disclose-deny is not in effect. + 1 - 99 : reserved for init.d script + 100 : init script ID (basename) unknown + init script not registered sucessfully through + update-rc.d or init script does not exist. + This error is fatal for most initscript systems. + 101 : action not allowed + requested action will not be performed because of + runlevel or local policy constrains, and + --disclose-deny is in effect. Note that a fallback + action is NOT considered "action not allowed", + unless --nofalback is in effect. + 102 : subsystem error + initscript (or policy) subsystem malfuncion. + (e.g. broken /sbin/runlevel). + Also, forced initscript execution due to + --try-anyway or --force failed. + 103 : syntax error + 104 : action allowed + --query is in effect; init script would be run if + not for --query. + 105 : behaviour uncertain + cannot determine if action should be carried out or + not, and --query in effect. + 106 : fallback action requested + the policy layer denied the requested action, and + supplied an allowed fallback action. diff --git a/doc/README.policy-rc.d b/doc/README.policy-rc.d new file mode 100644 index 0000000..232ebb6 --- /dev/null +++ b/doc/README.policy-rc.d @@ -0,0 +1,102 @@ + + +This is the internal documentation for policy-rc.d, as +written by Henrique M Holschuh <hmh@debian.org> + +This document can be found on the web as well at +http://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt + +There is also the Debian BTS entry for the invoke-rc.d policy change at +http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=76868 + + +POLICY-RC.D Policy layer (/usr/sbin/policy-rc.d) interface: +============================================================= + +Most Debian systems will not have this script as the need for a policy layer +is not very common. Most people using chroot jails just need an one-line +script which returns an exit status of 101 as the jailed +/usr/sbin/policy-rc.d script. + +The /usr/sbin/policy-rc.d file *must* be managed through the alternatives +system (/usr/sbin/update-alternatives) by any packages providing it. + +/usr/sbin/policy-rc.d [options] <initscript ID> <actions> [<runlevel>] +/usr/sbin/policy-rc.d [options] --list <initscript ID> [<runlevel> ...] + +Options: + --quiet + no error messages are generated. + + --list + instead of verifying policy, list (in a "human parseable" way) all + policies defined for the given initscript id (for all runlevels if no + runlevels are specified; otherwise, list it only for the runlevels + specified), as well as all known actions and their fallbacks for the + given initscript id (note that actions and fallback actions might be + global and not particular to a single initscript id). + +<actions> is a space-separated list of actions (usually only one). Note that +the list is passed in a single parameter and not as multiple parameters. + +The following actions are always known (even if specifying a policy for them +is not supported by whatever policy-rc.d system is in use): start, +[force-]stop, restart, [force-]reload, status. + +If an out-of-runlevel start or restart attempt is detected by invoke-rc.d, +the "start" or "restart" action will be changed to "(start)" or "(restart)" +respectively. This allows policy-rc.d to differentiate an out-of-runlevel +start/restart from a normal one. + +The runlevel parameters are optional. If a runlevel is not specified, it is +considered to be unknown/undefined. Note that for sysv-like initscript +systems, an undefined runlevel is very likely to cause a 105 exit status. + +A runlevel for update-rc.d is defined as a character string, of which the +usual INIT one-character runlevels are only a subset. It may contain +embedded blanks. + + stdout is used to output a single line containing fallback actions, + or to output --list results. + stderr is used to output error messages + stdin is not to be used, this is not an interactive interface. + + Exit status codes: + 0 - action allowed + 1 - unknown action (therefore, undefined policy) + 100 - unknown initscript id + 101 - action forbidden by policy + 102 - subsystem error + 103 - syntax error + 104 - [reserved] + 105 - behaviour uncertain, policy undefined. + 106 - action not allowed. Use the returned fallback actions + (which are implied to be "allowed") instead. + +When in doubt (policy-rc.d returned status 105 or status 1), invoke-rc.d +will assume an action is allowed, but it will warn the user of the problem. + +Returning fallback information: + +Fallback actions are returned in the first line sent to stdout (other lines +will be discarded). Multiple actions to be tried are allowed, and must be +separated by spaces. Multiple actions are carried out one at a time, until +one is sucessful. + +e.g.: returning status 106 and "restart stop" in stdout (without +the quotes) will cause invoke-rc.d to attempt action "restart", +and then only if "restart" failed, attempt action "stop". + +invoke-rc.d built-in policy rules: + +To shield policy-rc.d of the underlying initscript system (file-rc, links in +/etc/rc?.d or something else), invoke-rc.d implements the following built-in +rules: + + 1. action "start" out of runlevel is denied, + (policy-rc.d receives action "(start)" instead of "start"); + 2. action "restart" out of runlevel is denied, + (policy-rc.d receives action "(restart)" instead of "restart"); + 3. any action for a non-executable initscript is denied. + +Rule 3 is absolute, policy-rc.d cannot override it. diff --git a/man8/invoke-rc.d.rst b/man8/invoke-rc.d.rst new file mode 100644 index 0000000..fc21a73 --- /dev/null +++ b/man8/invoke-rc.d.rst @@ -0,0 +1,223 @@ +=================== + invoke-rc.d +=================== + +--------------------------------------------------------- +executes System-V style init script actions +--------------------------------------------------------- + +:Manual section: 8 +:Manual group: Debian GNU/Linux +:Author: + Henrique de Moraes Holschuh + +:Version: 1 March 2001 +:Copyright: 2001 Henrique de Moraes Holschuh +:License: GNU General Public License v2 or Later (GPLv2+) + +SYNOPSIS +======== + +``invoke-rc.d`` [*--quiet*] [*--force*] [*--try-anyway*] [*--disclose-deny*] +[*--query*] [*--no-fallback*] *name* *action* [*init script parameters...*] + + +``invoke-rc.d`` [*--help*] + +DESCRIPTION +=========== + +``invoke-rc.d`` +is a generic interface to execute System V style init script +``/etc/init.d/``\ *name* +actions, obeying runlevel constraints as well as any local +policies set by the system administrator. + +All access to the init scripts by Debian packages' maintainer +scripts should be done through +``invoke-rc.d``. + +This manpage documents only the usage and behavior of +``invoke-rc.d``. +For a discussion of the System V style init script arrangements please +see ``init``\(8\). +More information on invoke-rc.d can be found in the section on +runlevels and init.d scripts of the +*Debian Policy Manual*. + + +INIT SCRIPT ACTIONS +=================== + +The standard actions are: +*start*, *stop*, *force-stop*, *restart*, *try-restart*, *reload*, +*force-reload*, and *status*. +Other actions are accepted, but they can cause problems to +``policy-rc.d`` (see the ``INIT SCRIPT POLICY`` section), so +warnings are generated if the policy layer is active. + +Please note that not all init scripts will implement all +the actions listed above, and that the policy layer may +override an action to another action(s), or even deny it. + +Any extra parameters will be passed to the init script(s) being +executed. + +If an action must be carried out regardless of any local +policies, use the *--force* switch. + +OPTIONS +======= + +*--help* + Display usage help. + +*--quiet* + Quiet mode, no error messages are generated. + +*--force* + Tries to run the init script regardless of policy and + init script subsystem errors. + **Use of this option in Debian maintainer scripts is severely discouraged.** + +*--try-anyway* + Tries to run the init script if a non-fatal error is + detected. + +*--disclose-deny* + Return status code 101 instead of status code 0 if + the init script action is denied by the policy layer. + +*--query* + Returns one of the status codes 100-106. Does not + run the init script, and implies *--disclose-deny* + and *--no-fallback*. + +*--no-fallback* + Ignores any fallback action requests by the policy + layer. + **Warning:** + this is usually a very bad idea for any actions other + than start. + +*--skip-systemd-native* + Exits before doing anything if a systemd environment is detected + and the requested service is a native systemd unit. + This is useful for maintainer scripts that want to defer systemd + actions to ``deb-systemd-invoke``\(1p\) + +STATUS CODES +============ + +Should an init script be executed, ``invoke-rc.d`` +always returns the status code +returned by the init script. Init scripts should not return status codes in +the 100+ range (which is reserved in Debian and by the LSB). The status codes +returned by invoke-rc.d proper are: + +0 + *Success*. + Either the init script was run and returned exit status 0 (note + that a fallback action may have been run instead of the one given in the + command line), or it was not run because of runlevel/local policy constrains + and ``--disclose-deny`` is not in effect. + +1 - 99 + Reserved for init.d script, usually indicates a failure. + +100 + **Init script ID (**\ *name*\ **) unknown.** + This means the init script was not registered successfully through + ``update-rc.d`` or that the init script does not exist. + +101 + **Action not allowed**. + The requested action will not be performed because of runlevel or local + policy constraints. + +102 + **Subsystem error**. + Init script (or policy layer) subsystem malfunction. Also, forced + init script execution due to *--try-anyway* or *--force* + failed. + +103 + *Syntax error.* + +104 + *Action allowed*. + Init script would be run, but ``--query`` is in effect. + +105 + *Behavior uncertain*. + It cannot be determined if action should be carried out or not, and + ``--query`` + is in effect. + +106 + *Fallback action requested*. + The policy layer denied the requested action, and + supplied an allowed fallback action to be used instead. + + +INIT SCRIPT POLICY +================== + +``invoke-rc.d`` +introduces the concept of a policy layer which is used to verify if +an init script should be run or not, or if something else should be +done instead. This layer has various uses, the most immediate ones +being avoiding that package upgrades start daemons out-of-runlevel, +and that a package starts or stops daemons while inside a chroot +jail. + +The policy layer has the following abilities: deny or approve the +execution of an action; request that another action (called a +*fallback*) +is to be taken, instead of the action requested in invoke-rc.d's +command line; or request multiple actions to be tried in order, until +one of them succeeds (a multiple *fallback*). + +``invoke-rc.d`` +itself only pays attention to the current runlevel; it will block +any attempts to start a service in a runlevel in which the service is +disabled. Other policies are implemented with the use of the +``policy-rc.d`` +helper, and are only available if +``/usr/sbin/policy-rc.d`` +is installed in the system. + + +FILES +===== + +/etc/init.d/* + System V init scripts. + +/usr/sbin/policy-rc.d + Init script policy layer helper (not required). + +/etc/rc?.d/* + System V runlevel configuration. + +NOTES +===== + +``invoke-rc.d`` special cases the *status* +action, and returns exit status 4 instead of exit status 0 when +it is denied. + +BUGS +==== + +See http://bugs.debian.org/sysv-rc and +http://bugs.debian.org/init-system-helpers. + +SEE ALSO +======== + +| *Debian Policy manual*, +| ``/etc/init.d/skeleton``, +| ``update-rc.d``\(8\), +| ``init``\(8\), +| ``/usr/share/doc/init-system-helpers/README.policy-rc.d.gz`` diff --git a/man8/service.rst b/man8/service.rst new file mode 100644 index 0000000..8c43980 --- /dev/null +++ b/man8/service.rst @@ -0,0 +1,88 @@ +=================== + service +=================== + +--------------------------------------------------------- +run a System V init script +--------------------------------------------------------- + +:Manual section: 8 +:Manual group: System Manager's Manual +:Author: + Miloslav Trmac <mitr@redhat.com>, + Petter Reinholdtsen <pere@hungry.com> + +:Version: Jan 2006 +:Copyright: 2006 Red Hat, Inc., Petter Reinholdtsen <pere@hungry.com> +:License: GNU General Public License v2 (GPLv2) + + +SYNOPSIS +======== + + +``service`` *SCRIPT* *COMMAND* [*OPTIONS*] + +``service`` ``--status-all`` + +``service`` ``--help`` | ``-h`` | ``--version`` + + +DESCRIPTION +=========== + +``service`` runs a System V init script or systemd unit in as predictable an +environment as possible, removing most environment variables and with the +current working directory set to ``/``. + + +The +*SCRIPT* +parameter specifies a System V init script, located in */etc/init.d/SCRIPT*, +or the name of a systemd unit. The existence of a systemd unit of the same +name as a script in ``/etc/init.d`` will cause the unit to take precedence +over the init.d script. +The supported values of *COMMAND* depend on the invoked script. ``service`` +passes *COMMAND* and *OPTIONS* to the init script unmodified. For systemd +units, start, stop, status, and reload are passed through to their +systemctl/initctl equivalents. + +All scripts should support at least the ``start`` and ``stop`` commands. +As a special case, if *COMMAND* is ``--full-restart``, the script is run +twice, first with the ``stop`` command, then with the ``start`` +command. Note, that unlike ``update-rc.d``\(8\), ``service`` does not +check ``/usr/sbin/policy-rc.d``. + +``service --status-all`` runs all init scripts, in alphabetical order, with +the ``status`` command. The status is [ + ] for running services, [ - ] for +stopped services and [ ? ] for services without a ``status`` command. This +option only calls status for sysvinit jobs. + +EXIT CODES +========== + +``service`` calls the init script and returns the status returned by it. + +FILES +========== + +``/etc/init.d`` + The directory containing System V init scripts. + +``/{lib,run,etc}/systemd/system`` + The directories containing systemd units. + +ENVIRONMENT +=========== + +``LANG``, ``LANGUAGE``, ``LC_CTYPE``, ``LC_NUMERIC``, ``LC_TIME``, ``LC_COLLATE``, ``LC_MONETARY``, ``LC_MESSAGES``, ``LC_PAPER``, ``LC_NAME``, ``LC_ADDRESS``, ``LC_TELEPHONE``, ``LC_MEASUREMENT``, ``LC_IDENTIFICATION``, ``LC_ALL``, ``TERM``, ``PATH`` + The only environment variables passed to the init scripts. + +SEE ALSO +======== + +| */etc/init.d/skeleton* +| ``update-rc.d``\(8\) +| ``init``\(8\) +| ``invoke-rc.d``\(8\) +| ``systemctl``\(1\) diff --git a/man8/update-rc.d.rst b/man8/update-rc.d.rst new file mode 100644 index 0000000..b1da14e --- /dev/null +++ b/man8/update-rc.d.rst @@ -0,0 +1,249 @@ +=================== + update-rc.d +=================== + +--------------------------------------------------------- +install and remove System-V style init script links +--------------------------------------------------------- + +:Manual section: 8 +:Manual group: Debian GNU/Linux +:Author: + Ian Jackson, + Miquel van Smoorenburg + +:Version: 14 November 2005 +:Copyright: 2001 Henrique de Moraes Holschuh +:License: GNU General Public License v2 or Later (GPLv2+) + + +SYNOPSIS +========= + +``update-rc.d`` [*-f*] *name* ``remove`` + +``update-rc.d`` *name* ``defaults`` + +``update-rc.d`` *name* ``defaults-disabled`` + +``update-rc.d`` *name* ``disable|enable`` [ *S|2|3|4|5* ] + + +DESCRIPTION +=========== + +``update-rc.d`` updates the System V style init script links +``/etc/rc``\ *runlevel*\ ``.d/``\ *NNname* +whose target is the script +``/etc/init.d/``\ *name*. +These links are run by +``init`` +when it changes runlevels; they are generally used to start and stop +system services such as daemons. +*runlevel* +is one of the runlevels supported by +``init``, namely, ``0123456789S``, and +*NN* +is the two-digit sequence number that determines where in the sequence +``init`` +will run the scripts. + +This manpage documents only the usage and behaviour of +``update-rc.d``. +For a discussion of the System V style init script arrangements please +see +``init``\(8) +and the +*Debian Policy Manual*. + + +INSTALLING INIT SCRIPT LINKS +============================ + +update-rc.d requires dependency and runlevel information to be +provided in the init.d script LSB comment header of all init.d scripts. +See the insserv(8) manual page for details about the LSB header format. + +When run with the +``defaults`` +option, +``update-rc.d`` +makes links named +``/etc/rc``\ *runlevel*\ ``.d/[SK]``\ *NNname* +that point to the script +``/etc/init.d/``\ *name*, +using runlevel and dependency information from the init.d script LSB +comment header. + +When run with the +``defaults-disabled`` +option, +``update-rc.d`` +makes links named +``/etc/rc``\ *runlevel*\ ``.d/K``\ *NNname* +that point to the script +``/etc/init.d/``\ *name*, +using dependency information from the init.d script LSB comment header. +This means that the init.d script will be disabled (see below). + +If any files named +``/etc/rc``\ *runlevel*\ ``.d/[SK]??``\ *name* +already exist then +``update-rc.d`` +does nothing. +The program was written this way so that it will never +change an existing configuration, which may have been +customized by the system administrator. +The program will only install links if none are present, +i.e., +if it appears that the service has never been installed before. + +Older versions of +``update-rc.d`` +also supported +``start`` +and +``stop`` +options. These options are no longer supported, and are now +equivalent to the +``defaults`` +option. + +A common system administration error is to delete the links +with the thought that this will "disable" the service, i.e., +that this will prevent the service from being started. +However, if all links have been deleted then the next time +the package is upgraded, the package's +*postinst* +script will run +``update-rc.d`` +again and this will reinstall links at their factory default locations. +The correct way to disable services is to configure the +service as stopped in all runlevels in which it is started by default. +In the System V init system this means renaming +the service's symbolic links +from ``S`` to ``K``. +.P +The script +.BI /etc/init.d/ name +must exist before +``update-rc.d`` +is run to create the links. + +REMOVING SCRIPTS +================ + +When invoked with the +*remove* +option, update-rc.d removes any links in the +``/etc/rc``\ *runlevel*\ ``.d`` +directories to the script +``/etc/init.d/``\ *name*. +The script must have been deleted already. +If the script is still present then +``update-rc.d`` +aborts with an error message. +.P +``update-rc.d`` +is usually called from a package's post-removal script when that +script is given the +``purge`` +argument. +Any files in the +``/etc/rc``\ *runlevel*\ ``.d`` +directories that are not symbolic links to the script +``/etc/init.d/``\ *name* +will be left untouched. + +DISABLING INIT SCRIPT START LINKS +================================= + +When run with the +``disable`` [ *S|2|3|4|5* ] +options, +``update-rc.d`` +modifies existing runlevel links for the script +``/etc/init.d/``\ *name* +by renaming start links to stop links with a sequence number equal +to the difference of 100 minus the original sequence number. + +When run with the +``enable`` [ *S|2|3|4|5* ] +options, +``update-rc.d`` +modifies existing runlevel links for the script +``/etc/init.d/``\ *name* +by renaming stop links to start links with a sequence number equal +to the positive difference of current sequence number minus 100, thus +returning to the original sequence number that the script had been +installed with before disabling it. +.P +Both of these options only operate on start runlevel links of S, 2, +3, 4 or 5. If no start runlevel is specified after the disable or enable +keywords, the script will attempt to modify links in all start runlevels. + + +OPTIONS +======= + +-f + Force removal of symlinks even if + ``/etc/init.d/``\ *name* + still exists. + +EXAMPLES +======== + +Insert links using the defaults: + + ``update-rc.d foobar defaults`` + +The equivalent dependency header would have start and stop +dependencies on $remote_fs and $syslog, and start in +runlevels 2-5 and stop in runlevels 0, 1 and 6. + + +Remove all links for a script (assuming foobar has been deleted +already): + + ``update-rc.d foobar remove`` + +Example of disabling a service: + + ``update-rc.d foobar disable`` + +Example of a command for installing a system initialization-and-shutdown script: + + ``update-rc.d foobar defaults`` + +Example of a command for disabling a system initialization-and-shutdown script: + + ``update-rc.d foobar disable`` + +BUGS +==== + +See http://bugs.debian.org/sysv-rc and +http://bugs.debian.org/init-system-helpers. + +FILES +===== + + +``/etc/init.d/`` + The directory containing the actual init scripts. + +``/etc/rc?.d/`` + The directories containing the links used by ``init`` + and managed by ``update-rc.d .`` + +``/etc/init.d/skeleton`` + Model for use by writers of ``init.d`` scripts. + +SEE ALSO +======== + +| *Debian Policy Manual*, +| ``/etc/init.d/skeleton``, +| ``insserv``\(8), +| ``init``\(8) diff --git a/script/deb-systemd-helper b/script/deb-systemd-helper new file mode 100755 index 0000000..5e7e167 --- /dev/null +++ b/script/deb-systemd-helper @@ -0,0 +1,691 @@ +#!/usr/bin/perl +# vim:ts=4:sw=4:expandtab +# © 2013-2014 Michael Stapelberg <stapelberg@debian.org> +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Michael Stapelberg nor the +# names of contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# . +# THIS SOFTWARE IS PROVIDED BY Michael Stapelberg ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Michael Stapelberg BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=head1 NAME + +deb-systemd-helper - subset of systemctl for machines not running systemd + +=head1 SYNOPSIS + +B<deb-systemd-helper> enable | disable | purge | mask | unmask | is-enabled | was-enabled | debian-installed | update-state | reenable S<I<unit file> ...> + +=head1 DESCRIPTION + +B<deb-systemd-helper> is a Debian-specific helper script which re-implements +the enable, disable, is-enabled and reenable commands from systemctl. + +The "enable" action will only be performed once (when first installing the +package). On the first "enable", a state file is created which will be deleted +upon "purge". + +The "mask" action will keep state on whether the service was enabled/disabled +before and will properly return to that state on "unmask". + +The "was-enabled" action is not present in systemctl, but is required in Debian +so that we can figure out whether a service was enabled before we installed an +updated service file. See http://bugs.debian.org/717603 for details. + +The "debian-installed" action is also not present in systemctl. It returns 0 if +the state file of at least one of the given units is present. + +The "update-state" action is also not present in systemctl. It updates +B<deb-systemd-helper>'s state file, removing obsolete entries (e.g. service +files that are no longer shipped by the package) and adding new entries (e.g. +new service files shipped by the package) without enabling them. + +B<deb-systemd-helper> is intended to be used from maintscripts to enable +systemd unit files. It is specifically NOT intended to be used interactively by +users. Instead, users should run systemd and use systemctl, or not bother about +the systemd enabled state in case they are not running systemd. + +=head1 ENVIRONMENT + +=over 4 + +=item B<_DEB_SYSTEMD_HELPER_DEBUG> + +If you export _DEB_SYSTEMD_HELPER_DEBUG=1, deb-systemd-helper will print debug +messages to stderr (thus visible in dpkg runs). Please include these when +filing a bugreport. + +=item B<DPKG_ROOT> + +Instead of working on the filesystem root /, perform all operations on a chroot +system in the directory given by DPKG_ROOT. + +=back + +=cut + +use strict; +use warnings; +use File::Path qw(make_path); # in core since Perl 5.001 +use File::Basename; # in core since Perl 5 +use File::Temp qw(tempfile); # in core since Perl 5.6.1 +use Getopt::Long; # in core since Perl 5 +# Make Data::Dumper::Dumper available if present (not present on systems that +# only have perl-base, not perl). +eval { require Data::Dumper; } or *Data::Dumper::Dumper = sub { "no Data::Dumper" }; + +my $dpkg_root = $ENV{DPKG_ROOT} // ''; + +use constant { + SYSTEM_INSTANCE_ENABLED_STATE_DIR => '/var/lib/systemd/deb-systemd-helper-enabled', + USER_INSTANCE_ENABLED_STATE_DIR => '/var/lib/systemd/deb-systemd-user-helper-enabled', + SYSTEM_INSTANCE_MASKED_STATE_DIR => '/var/lib/systemd/deb-systemd-helper-masked', + USER_INSTANCE_MASKED_STATE_DIR => '/var/lib/systemd/deb-systemd-user-helper-masked', +}; + +my $quiet = 0; +my $instance = 'system'; +my $enabled_state_dir = $dpkg_root . SYSTEM_INSTANCE_ENABLED_STATE_DIR; +my $masked_state_dir = $dpkg_root . SYSTEM_INSTANCE_MASKED_STATE_DIR; + +# Globals are bad, but in this specific case, it really makes things much +# easier to write and understand. +my $changed_sth; +my $has_systemctl = -x "$dpkg_root/bin/systemctl" || -x "$dpkg_root/usr/bin/systemctl"; + +sub assertdpkgroot { + my ($path, $msg) = @_; + if (length $ENV{DPKG_ROOT}) { + if ($path !~ /^\Q$dpkg_root\E/) { + error("doesn't start with dpkg_root: $path $msg"); + } + if ($path =~ /^\Q$dpkg_root$dpkg_root\E/) { + error("double dpkg_root: $path $msg"); + } + } +} + +sub assertnotdpkgroot { + my ($path, $msg) = @_; + if (length $ENV{DPKG_ROOT}) { + if ($path =~ /^\Q$dpkg_root\E/) { + error("starts with dpkg_root: $path $msg"); + } + } +} + +sub error { + print STDERR "$0: error: @_\n"; + exit (1); +} + +sub debug { + my ($msg) = @_; + return if !defined($ENV{_DEB_SYSTEMD_HELPER_DEBUG}) || $ENV{_DEB_SYSTEMD_HELPER_DEBUG} != 1; + print STDERR "(deb-systemd-helper DEBUG) $msg\n"; +} + +sub is_purge { + return (defined($ENV{_DEB_SYSTEMD_HELPER_PURGE}) && $ENV{_DEB_SYSTEMD_HELPER_PURGE} == 1) +} + +sub find_unit { + my ($scriptname) = @_; + + my $service_path = $scriptname; + + if (-f "$dpkg_root/etc/systemd/$instance/$scriptname") { + $service_path = "/etc/systemd/$instance/$scriptname"; + } elsif (-f "$dpkg_root/lib/systemd/$instance/$scriptname") { + $service_path = "/lib/systemd/$instance/$scriptname"; + } elsif (-f "$dpkg_root/usr/lib/systemd/$instance/$scriptname") { + $service_path = "/usr/lib/systemd/$instance/$scriptname"; + } + + return $service_path; +} + +sub dsh_state_path { + my ($scriptname) = @_; + return $enabled_state_dir . '/' . basename($scriptname) . '.dsh-also'; +} + +sub state_file_entries { + my ($dsh_state) = @_; + debug "Reading state file $dsh_state"; + my @entries; + if (open(my $fh, '<', $dsh_state)) { + @entries = map { chomp; "$dpkg_root$_" } <$fh>; + close($fh); + } + return @entries; +} + +# Writes $service_link into $dsh_state unless it’s already in there. +sub record_in_statefile { + my ($dsh_state, $service_link) = @_; + + assertdpkgroot($dsh_state, "record_in_statefile"); + assertnotdpkgroot($service_link, "record_in_statefile"); + + # Appending a newline makes the following code simpler; we can skip + # chomp()ing and appending newlines in every print. + $service_link .= "\n"; + + make_path(dirname($dsh_state)); + my $line_exists; + my ($outfh, $tmpname) = tempfile('.stateXXXXX', + DIR => dirname($dsh_state), + SUFFIX => '.tmp', + UNLINK => 0); + chmod(0644, $tmpname); + if (-e $dsh_state) { + open(my $infh, '<', $dsh_state) or error("unable to read from $dsh_state"); + while (<$infh>) { + $line_exists = 1 if $_ eq $service_link; + print $outfh $_; + } + close($infh); + } + print $outfh $service_link unless $line_exists; + close($outfh) or error("unable to close $tmpname"); + + debug "Renaming temp file $tmpname to state file $dsh_state"; + rename($tmpname, $dsh_state) or + error("Unable to move $tmpname to $dsh_state"); +} + +# Gets the transitive closure of links, i.e. all links that need to be created +# when enabling this service file. Not straight-forward because service files +# can refer to other service files using Also=. +sub get_link_closure { + my ($scriptname, $service_path, @visited) = @_; + assertnotdpkgroot($service_path, "get_link_closure"); + + my @links; + my @wants_dirs; + + my $unit_name = basename($service_path); + my $wanted_target = $unit_name; + + # The keys parsed from the unit file below can only have unit names + # as values. Since unit names can't have whitespace in systemd, + # simply use split and strip any leading/trailing quotes. See + # systemd-escape(1) for examples of valid unit names. + open my $fh, '<', "$dpkg_root$service_path" or error("unable to read $dpkg_root$service_path"); + while (my $line = <$fh>) { + chomp($line); + my $service_link; + + if ($line =~ /^\s*(WantedBy|RequiredBy)=(.+)$/i) { + for my $value (split(/\s+/, $2)) { + $value =~ s/^(["'])(.*)\g1$/$2/; + my $wants_dir = "/etc/systemd/$instance/$value"; + $wants_dir .= '.wants' if $1 eq 'WantedBy'; + $wants_dir .= '.requires' if $1 eq 'RequiredBy'; + push @wants_dirs, "$wants_dir/"; + } + } + + if ($line =~ /^\s*Also=(.+)$/i) { + for my $value (split(/\s+/, $1)) { + $value =~ s/^(["'])(.*)\g1$/$2/; + if ($value ne $unit_name and not grep $_ eq $value, @visited) { + # We can end up in an infinite recursion, so remember what units we + # already processed to break it + push @visited, $value; + push @links, get_link_closure($value, find_unit($value), @visited); + } + } + } + + if ($line =~ /^\s*Alias=(.+)$/i) { + for my $value (split(/\s+/, $1)) { + $value =~ s/^(["'])(.*)\g1$/$2/; + if ($value ne $unit_name) { + push @links, { dest => $service_path, src => "/etc/systemd/$instance/$1" }; + } + } + } + + if ($line =~ /^\s*DefaultInstance=\s*(["']?+)(.+)\g1\s*$/i) { + $wanted_target = $2; + $wanted_target = $unit_name =~ s/^(.*\@)(\.\w+)$/$1$wanted_target$2/r; + } + } + close($fh); + + for my $wants_dir (@wants_dirs) { + push @links, { dest => $service_path, src => $wants_dir . $wanted_target }; + } + + return @links; +} + +sub all_links_installed { + my ($scriptname, $service_path) = @_; + + my @links = get_link_closure($scriptname, $service_path); + foreach my $link (@links) { + assertnotdpkgroot($link->{src}, "all_links_installed"); + } + my @missing_links = grep { ! -l "$dpkg_root$_->{src}" } @links; + + return (@missing_links == 0); +} + +sub no_link_installed { + my ($scriptname, $service_path) = @_; + + my @links = get_link_closure($scriptname, $service_path); + foreach my $link (@links) { + assertnotdpkgroot($link->{src}, "all_links_installed"); + } + my @existing_links = grep { -l "$dpkg_root$_->{src}" } @links; + + return (@existing_links == 0); +} + +sub enable { + my ($scriptname, $service_path) = @_; + if ($has_systemctl) { + # We use 'systemctl preset' on the initial installation only. + # On upgrade, we manually add the missing symlinks only if the + # service already has some links installed. Using 'systemctl + # preset' allows administrators and downstreams to alter the + # enable policy using systemd-native tools. + my $create_links = 0; + if (debian_installed($scriptname)) { + $create_links = 1 unless no_link_installed($scriptname, $service_path); + } else { + debug "Using systemctl preset to enable $scriptname"; + my $systemd_root = '/'; + if ($dpkg_root ne '') { + $systemd_root = $dpkg_root; + } + system("systemctl", + "--root=$systemd_root", + $instance eq "user" ? "--global" : "--system", + "--preset-mode=enable-only", + "preset", $scriptname) == 0 + or error("systemctl preset failed on $scriptname: $!"); + } + make_systemd_links($scriptname, $service_path, create_links => $create_links); + } else { + # We create all the symlinks ourselves + make_systemd_links($scriptname, $service_path); + } +} + +sub make_systemd_links { + my ($scriptname, $service_path, %opts) = @_; + $opts{'create_links'} //= 1; + + my $dsh_state = dsh_state_path($scriptname); + + my @links = get_link_closure($scriptname, $service_path); + for my $link (@links) { + my $service_path = $link->{dest}; + my $service_link = $link->{src}; + + record_in_statefile($dsh_state, $service_link); + + my $statefile = $service_link; + $statefile =~ s,^/etc/systemd/$instance/,$enabled_state_dir/,; + $service_link = "$dpkg_root$service_link"; + assertdpkgroot($statefile, "make_systemd_links"); + assertdpkgroot($service_link, "make_systemd_links"); + assertnotdpkgroot($service_path, "make_systemd_links"); + next if -e $statefile; + + if ($opts{'create_links'} && ! -l $service_link) { + make_path(dirname($service_link)); + symlink($service_path, $service_link) or + error("unable to link $service_link to $service_path: $!"); + $changed_sth = 1; + } + + # Store the fact that we ran enable for this service_path, + # so that we can skip enable the next time. + # This allows us to call deb-systemd-helper unconditionally + # and still only enable unit files on the initial installation + # of a package. + make_path(dirname($statefile)); + open(my $fh, '>>', $statefile) or error("Failed to create/touch $statefile"); + close($fh) or error("Failed to create/touch $statefile"); + } + +} + +# In contrary to make_systemd_links(), which only modifies the state file in an +# append-only fashion, update_state() can also remove entries from the state +# file. +# +# The distinction is important because update_state() should only be called +# when the unit file(s) are guaranteed to be on-disk, e.g. on package updates, +# but not on package removals. +sub update_state { + my ($scriptname, $service_path) = @_; + + my $dsh_state = dsh_state_path($scriptname); + my @links = get_link_closure($scriptname, $service_path); + assertdpkgroot($dsh_state, "update_state"); + + debug "Old state file contents: " . + Data::Dumper::Dumper([ state_file_entries($dsh_state) ]); + + make_path(dirname($dsh_state)); + my ($outfh, $tmpname) = tempfile('.stateXXXXX', + DIR => dirname($dsh_state), + SUFFIX => '.tmp', + UNLINK => 0); + chmod(0644, $tmpname); + for my $link (@links) { + assertnotdpkgroot($link->{src}, "update_state"); + print $outfh $link->{src} . "\n"; + } + close($outfh) or error("Failed to close $tmpname"); + + debug "Renaming temp file $tmpname to state file $dsh_state"; + rename($tmpname, $dsh_state) or + error("Unable to move $tmpname to $dsh_state"); + + debug "New state file contents: " . + Data::Dumper::Dumper([ state_file_entries($dsh_state) ]); +} + +sub was_enabled { + my ($scriptname) = @_; + + my @entries = state_file_entries(dsh_state_path($scriptname)); + debug "Contents: " . Data::Dumper::Dumper(\@entries); + + for my $link (@entries) { + assertdpkgroot($link, "was_enabled"); + if (! -l $link) { + debug "Link $link is missing, considering $scriptname was-disabled."; + return 0; + } + } + + debug "All links present, considering $scriptname was-enabled."; + return 1; +} + +sub debian_installed { + my ($scriptname) = @_; + return -f dsh_state_path($scriptname); +} + +sub remove_links { + my ($service_path) = @_; + + my $dsh_state = dsh_state_path($service_path); + my @entries = state_file_entries($dsh_state); + debug "Contents: " . Data::Dumper::Dumper(\@entries); + assertdpkgroot($dsh_state, "remove_links"); + assertnotdpkgroot($service_path, "remove_links"); + + if (is_purge()) { + unlink($dsh_state) if -e $dsh_state; + } + + # Also disable all the units which were enabled when this one was enabled. + for my $link (@entries) { + # Delete the corresponding state file: + # • Always when purging + # • If the user did not disable (= link still exists) the service. + # If we don’t do this, the link will be deleted a few lines down, + # but not re-created when re-installing the package. + assertdpkgroot($link, "remove_links"); + if (is_purge() || -l $link) { + my $link_state = $link; + $link_state =~ s,^\Q$dpkg_root\E/etc/systemd/$instance/,$enabled_state_dir/,; + unlink($link_state); + } + + next unless -l $link; + unlink($link) or + print STDERR "$0: unable to remove '$link': $!\n"; + + $changed_sth = 1; + } + + # Read $service_path, recurse for all Also= units. + # This might not work when $service_path was already deleted, + # i.e. after apt-get remove. In this case we just return + # silently in order to not confuse the user about whether + # disabling actually worked or not — the case is handled by + # dh_installsystemd generating an appropriate disable + # command by parsing the service file at debhelper-time. + open(my $fh, '<', "$dpkg_root$service_path") or return; + while (my $line = <$fh>) { + chomp($line); + my $service_link; + + if ($line =~ /^\s*Also=(.+)$/i) { + remove_links(find_unit($1)); + } + } + close($fh); +} + +# Recursively deletes a directory structure, if all (!) components are empty, +# e.g. to clean up after purging. +sub rmdir_if_empty { + my ($dir) = @_; + + debug "rmdir_if_empty $dir"; + + rmdir_if_empty($_) for (grep { -d } <$dir/*>); + + if (!rmdir($dir)) { + debug "rmdir($dir) failed ($!)"; + } +} + +sub mask_service { + my ($scriptname, $service_path) = @_; + + my $mask_link = "$dpkg_root/etc/systemd/$instance/" . basename($service_path); + + if (-e $mask_link) { + # If the link already exists, don’t do anything. + return if -l $mask_link && readlink($mask_link) eq '/dev/null'; + + # If the file already exists, the user most likely copied the .service + # file to /etc/ to change it in some way. In this case we don’t need to + # mask the .service in the first place, since it will not be removed by + # dpkg. + debug "$mask_link already exists, not masking."; + return; + } + + make_path(dirname($mask_link)); + # clean up after possible leftovers from Alias= to self (LP#1439793) + unlink($mask_link); + symlink('/dev/null', $mask_link) or + error("unable to link $mask_link to /dev/null: $!"); + $changed_sth = 1; + + my $statefile = $mask_link; + $statefile =~ s,^\Q$dpkg_root\E/etc/systemd/$instance/,$masked_state_dir/,; + + # Store the fact that we masked this service, so that we can unmask it on + # installation time. We cannot unconditionally unmask because that would + # interfere with the user’s decision to mask a service. + make_path(dirname($statefile)); + open(my $fh, '>>', $statefile) or error("Failed to create/touch $statefile"); + close($fh) or error("Failed to create/touch $statefile"); +} + +sub unmask_service { + my ($scriptname, $service_path) = @_; + + my $mask_link = "$dpkg_root/etc/systemd/$instance/" . basename($service_path); + + # Not masked? Nothing to do. + return unless -e $mask_link; + + if (! -l $mask_link || readlink($mask_link) ne '/dev/null') { + debug "Not unmasking $mask_link because it is not a link to /dev/null"; + return; + } + + my $statefile = $mask_link; + $statefile =~ s,^\Q$dpkg_root\E/etc/systemd/$instance/,$masked_state_dir/,; + + if (! -e $statefile) { + debug "Not unmasking $mask_link because the state file $statefile does not exist"; + return; + } + + unlink($mask_link) or + error("unable to remove $mask_link: $!"); + $changed_sth = 1; + unlink($statefile); +} + +my $result = GetOptions( + "quiet" => \$quiet, + "user" => sub { $instance = 'user'; }, + "system" => sub { $instance = 'system'; }, # default +); + +if ($instance eq 'user') { + debug "is user unit = yes"; + $enabled_state_dir = $dpkg_root . USER_INSTANCE_ENABLED_STATE_DIR; + $masked_state_dir = $dpkg_root . USER_INSTANCE_MASKED_STATE_DIR; +} + +my $action = shift; +if (!defined($action)) { + # Called without arguments. Explain that this script should not be run interactively. + print "$0 is a program which should be called by dpkg maintscripts only.\n"; + print "Please do not run it interactively, ever. Also see the manpage deb-systemd-helper(1).\n"; + exit 0; +} + +if (!$ENV{DPKG_MAINTSCRIPT_PACKAGE}) { + print STDERR "$0 was not called from dpkg. Exiting.\n"; + exit 1; +} + +if ($action eq 'purge') { + $ENV{_DEB_SYSTEMD_HELPER_PURGE} = 1; + $action = 'disable'; +} + +debug "is purge = " . (is_purge() ? "yes" : "no"); + +my $rc = 0; +if ($action eq 'is-enabled' || + $action eq 'was-enabled' || + $action eq 'debian-installed') { + $rc = 1; +} +for my $scriptname (@ARGV) { + my $service_path = find_unit($scriptname); + + debug "action = $action, scriptname = $scriptname, service_path = $service_path"; + + if ($action eq 'is-enabled') { + my $enabled = all_links_installed($scriptname, $service_path); + print STDERR ($enabled ? "enabled\n" : "disabled\n") unless $quiet; + $rc = 0 if $enabled; + } + + # was-enabled is the same as is-enabled, but only considers links recorded + # in the state file. This is useful after package upgrades, to determine + # whether the unit file was enabled before upgrading, even if the unit file + # has changed and is not entirely enabled currently (due to a new Alias= + # line for example). + # + # If all machines were running systemd, this issue would not be present + # because is-enabled would query systemd, which would not have picked up + # the new unit file yet. + if ($action eq 'was-enabled') { + my $enabled = was_enabled($scriptname); + print STDERR ($enabled ? "enabled\n" : "disabled\n") unless $quiet; + $rc = 0 if $enabled; + } + + if ($action eq 'update-state') { + update_state($scriptname, $service_path); + } + + if ($action eq 'debian-installed') { + $rc = 0 if debian_installed($scriptname); + } + + if ($action eq 'reenable') { + remove_links($service_path); + make_systemd_links($scriptname, $service_path); + } + + if ($action eq 'disable') { + remove_links($service_path); + # Clean up the state dir if it’s empty, or at least clean up all empty + # subdirectories. Necessary to cleanly pass a piuparts run. + rmdir_if_empty($dpkg_root . SYSTEM_INSTANCE_ENABLED_STATE_DIR); + rmdir_if_empty($dpkg_root . USER_INSTANCE_ENABLED_STATE_DIR); + + # Same with directories below /etc/systemd, where we create symlinks. + # If systemd is not installed (and no other package shipping service + # files), this would make piuparts fail, too. + rmdir_if_empty($_) for (grep { -d } <$dpkg_root/etc/systemd/system/*>); + rmdir_if_empty($_) for (grep { -d } <$dpkg_root/etc/systemd/user/*>); + } + + if ($action eq 'enable') { + enable($scriptname, $service_path); + } + + if ($action eq 'mask') { + mask_service($scriptname, $service_path); + } + + if ($action eq 'unmask') { + unmask_service($scriptname, $service_path); + # Clean up the state dir if it’s empty, or at least clean up all empty + # subdirectories. Necessary to cleanly pass a piuparts run. + rmdir_if_empty($dpkg_root . SYSTEM_INSTANCE_MASKED_STATE_DIR); + rmdir_if_empty($dpkg_root . USER_INSTANCE_MASKED_STATE_DIR); + } +} + +# If we changed anything and this machine is running systemd, tell +# systemd to reload so that it will immediately pick up our +# changes. +if (!length $ENV{DPKG_ROOT} && $changed_sth && $instance eq 'system' && -d "/run/systemd/system") { + system("systemctl", "daemon-reload"); +} + +exit $rc; + +=head1 AUTHOR + +Michael Stapelberg <stapelberg@debian.org> + +=cut diff --git a/script/deb-systemd-invoke b/script/deb-systemd-invoke new file mode 100755 index 0000000..6d49249 --- /dev/null +++ b/script/deb-systemd-invoke @@ -0,0 +1,187 @@ +#!/usr/bin/perl +# vim:ts=4:sw=4:expandtab +# © 2013 Michael Stapelberg <stapelberg@debian.org> +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of Michael Stapelberg nor the +# names of contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# . +# THIS SOFTWARE IS PROVIDED BY Michael Stapelberg ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Michael Stapelberg BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=head1 NAME + +deb-systemd-invoke - wrapper around systemctl, respecting policy-rc.d + +=head1 SYNOPSIS + +B<deb-systemd-invoke> [B<--user>] start|stop|restart S<I<unit file> ...> +B<deb-systemd-invoke> [B<--user>] [B<--no-dbus>] daemon-reload|daemon-reexec + +=head1 DESCRIPTION + +B<deb-systemd-invoke> is a Debian-specific helper script which asks +/usr/sbin/policy-rc.d before performing a systemctl call. + +B<deb-systemd-invoke> is intended to be used from maintscripts to manage +systemd unit files. It is specifically NOT intended to be used interactively by +users. Instead, users should run systemd and use systemctl, or not bother about +the systemd enabled state in case they are not running systemd. + +=cut + +use strict; +use warnings; +use Getopt::Long; # in core since Perl 5 + +if (@ARGV < 2) { + print STDERR "Syntax: $0 <action> [<unit file> [<unit file> ...]]\n"; + exit 1; +} + +my $is_system = 1; +my $use_dbus = 1; +my @instances = (); +my $result = GetOptions( + "user" => sub { $is_system = 0; }, + "system" => sub { $is_system = 1; }, # default + "no-dbus" => sub { $use_dbus = 0; }, +); + +my $policyhelper = '/usr/sbin/policy-rc.d'; +if (length $ENV{DPKG_ROOT}) { + $policyhelper = $ENV{DPKG_ROOT} . $policyhelper; +} +my @units = @ARGV; +my $action = shift @units; +if (-x $policyhelper) { + for my $unit (@units) { + system(qq|$policyhelper $unit "$action"|); + + # 0 or 104 means run + # 101 means do not run + my $exitcode = ($? >> 8); + if ($exitcode == 101) { + print STDERR "$policyhelper returned 101, not running '" . join(' ', @ARGV) . "'\n"; + exit 0; + } elsif ($exitcode != 104 && $exitcode != 0) { + print STDERR "deb-systemd-invoke only supports $policyhelper return codes 0, 101, and 104!\n"; + print STDERR "Got return code $exitcode, ignoring.\n"; + } + } +} + +if (!$is_system) { + # '--machine <ID>@' was added in v250 and v249.10, before that we can't talk to arbitrary user instances + my $systemctl_version = `systemctl --version --quiet | sed -n -r "s/systemd ([0-9]+) \\(.*/\\1/p"`; + chomp ($systemctl_version); + if (system('dpkg', '--compare-versions', $systemctl_version, 'ge', '249') != 0) { + print STDERR "systemctl version $systemctl_version does not support acting on user instance, skipping\n"; + exit 0; + } + + # Each user instance of the manager has a corresponding user@<id<.service unit. + # Get the full list of IDs, so that we can talk to each user instance to start/stop + # user units. + @instances = `systemctl --no-legend --quiet list-units 'user@*' | sed -n -r 's/.*user@([0-9]+).service.*/\\1/p'`; +} else { + push @instances, 'system'; +} + +# If the job is disabled and is not currently running, the job is not started or restarted. +# However, if the job is disabled but has been forced into the running state, we *do* stop +# and restart it since this is expected behaviour for the admin who forced the start. +# We don't autostart static units either. +if ($action eq "start" || $action eq "restart") { + my $global_exit_code = 0; + my @start_units = (); + + for my $instance (@instances) { + my @instance_args = (); + + if ($instance eq 'system') { + push @instance_args, '--system'; + } else { + chomp ($instance); + push @instance_args, '--user', '--machine', "$instance@"; + } + + for my $unit (@units) { + my $unit_installed = 0; + my $enabled_output = `systemctl @instance_args is-enabled -- '$unit'`; + # matching enabled and enabled-runtime as an installed non static unit + if ($enabled_output =~ /enabled/) { + $unit_installed = 1; + } + system('systemctl', @instance_args, '--quiet', 'is-active', '--', $unit); + my $unit_active = $?>>8 == 0 ? 1 : 0; + if (!$unit_installed && $action eq "start") { + print STDERR "$unit is a disabled or a static unit, not starting it.\n"; + } elsif (!$unit_installed && !$unit_active && $action eq "restart") { + print STDERR "$unit is a disabled or a static unit not running, not starting it.\n"; + } + else { + push @start_units, $unit; + } + } + if (@start_units) { + system('systemctl', '--quiet', @instance_args, $action, @start_units) == 0 or die("Could not execute systemctl: $!"); + } + } + exit(0); +} elsif ($action eq "stop" && !$is_system) { + my $global_exit_code = 0; + + for my $instance (@instances) { + chomp ($instance); + system('systemctl', '--quiet', '--user', '--machine', "$instance@", $action, @units); + } + exit(0); +} elsif (($action eq "daemon-reload" || $action eq "daemon-reexec") && !$is_system && $use_dbus) { + my $global_exit_code = 0; + + for my $instance (@instances) { + chomp ($instance); + system('systemctl', '--quiet', '--user', '--machine', "$instance@", $action); + } + exit(0); +} elsif (($action eq "daemon-reload" || $action eq "daemon-reexec") && !$use_dbus) { + my $global_exit_code = 0; + my $signal; + + if ($action eq "daemon-reload") { + $signal = 'SIGHUP'; + } else { + $signal = 'SIGRTMIN+25'; + } + + if ($is_system) { + system('kill', '-s', $signal, '1'); + } else { + system('systemctl', '--quiet', 'kill', '--kill-whom=main', '--signal', $signal, 'user@*.service'); + } + + exit(0); +} else { + exec('systemctl', @ARGV); +} diff --git a/script/invoke-rc.d b/script/invoke-rc.d new file mode 100755 index 0000000..ca1bdbe --- /dev/null +++ b/script/invoke-rc.d @@ -0,0 +1,574 @@ +#!/bin/sh +# vim: ft=sh +# +# invoke-rc.d.sysvinit - Executes initscript actions +# +# SysVinit /etc/rc?.d version for Debian's sysvinit package +# +# Copyright (C) 2000,2001 Henrique de Moraes Holschuh <hmh@debian.org> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +# Constants +RUNLEVELHELPER=/sbin/runlevel +POLICYHELPER=$DPKG_ROOT/usr/sbin/policy-rc.d +INITDPREFIX=/etc/init.d/ +RCDPREFIX=/etc/rc + +# Options +BEQUIET= +MODE= +ACTION= +FALLBACK= +NOFALLBACK= +FORCE= +RETRY= +RETURNFAILURE= +RC= +is_systemd= +is_openrc= +SKIP_SYSTEMD_NATIVE= + +# Shell options +set +e + +dohelp () { + # + # outputs help and usage + # +cat <<EOF + +invoke-rc.d, Debian/SysVinit (/etc/rc?.d) initscript subsystem. +Copyright (c) 2000,2001 Henrique de Moraes Holschuh <hmh@debian.org> + +Usage: + invoke-rc.d [options] <basename> <action> [extra parameters] + + basename - Initscript ID, as per update-rc.d(8) + action - Initscript action. Known actions are: + start, [force-]stop, [try-]restart, + [force-]reload, status + WARNING: not all initscripts implement all of the above actions. + + extra parameters are passed as is to the initscript, following + the action (first initscript parameter). + +Options: + --quiet + Quiet mode, no error messages are generated. + --force + Try to run the initscript regardless of policy and subsystem + non-fatal errors. + --try-anyway + Try to run init script even if a non-fatal error is found. + --disclose-deny + Return status code 101 instead of status code 0 if + initscript action is denied by local policy rules or + runlevel constrains. + --query + Returns one of status codes 100-106, does not run + the initscript. Implies --disclose-deny and --no-fallback. + --no-fallback + Ignores any fallback action requests by the policy layer. + Warning: this is usually a very *bad* idea for any actions + other than "start". + --skip-systemd-native + Exits before doing anything if a systemd environment is detected + and the requested service is a native systemd unit. + This is useful for maintainer scripts that want to defer systemd + actions to deb-systemd-invoke + --help + Outputs help message to stdout + +EOF +} + +printerror () { + # + # prints an error message + # $* - error message + # +if test x${BEQUIET} = x ; then + echo `basename $0`: "$*" >&2 +fi +} + +formataction () { + # + # formats a list in $* into $printaction + # for human-friendly printing to stderr + # and sets $naction to action or actions + # +printaction=`echo $* | sed 's/ /, /g'` +if test $# -eq 1 ; then + naction=action +else + naction=actions +fi +} + +querypolicy () { + # + # queries policy database + # returns: $RC = 104 - ok, run + # $RC = 101 - ok, do not run + # other - exit with status $RC, maybe run if $RETRY + # initial status of $RC is taken into account. + # + +policyaction="${ACTION}" +if test x${RC} = "x101" ; then + if test "${ACTION}" = "start" || test "${ACTION}" = "restart" || test "${ACTION}" = "try-restart"; then + policyaction="(${ACTION})" + fi +fi + +if test "x${POLICYHELPER}" != x && test -x "${POLICYHELPER}" ; then + FALLBACK=`${POLICYHELPER} ${BEQUIET} ${INITSCRIPTID} "${policyaction}" ${RL}` + RC=$? + formataction ${ACTION} + case ${RC} in + 0) RC=104 + ;; + 1) RC=105 + ;; + 101) if test x${FORCE} != x ; then + printerror Overriding policy-rc.d denied execution of ${printaction}. + RC=104 + else + printerror policy-rc.d denied execution of ${printaction}. + fi + ;; + esac + if test x${MODE} != xquery ; then + case ${RC} in + 105) printerror policy-rc.d query returned \"behaviour undefined\", + printerror assuming \"${printaction}\" is allowed. + RC=104 + ;; + 106) formataction ${FALLBACK} + if test x${FORCE} = x ; then + if test x${NOFALLBACK} = x ; then + ACTION="${FALLBACK}" + printerror executing ${naction} \"${printaction}\" instead due to policy-rc.d request. + RC=104 + else + printerror ignoring policy-rc.d fallback request: ${printaction}. + RC=101 + fi + else + printerror ignoring policy-rc.d fallback request: ${printaction}. + RC=104 + fi + ;; + esac + fi + case ${RC} in + 100|101|102|103|104|105|106) ;; + *) printerror WARNING: policy-rc.d returned unexpected error status ${RC}, 102 used instead. + RC=102 + ;; + esac +else + if test ! -e "/sbin/init" ; then + if test x${FORCE} != x ; then + printerror "WARNING: No init system and policy-rc.d missing, but force specified so proceeding." + else + printerror "WARNING: No init system and policy-rc.d missing! Defaulting to block." + RC=101 + fi + fi + + if test x${RC} = x ; then + RC=104 + fi +fi +return +} + +verifyparameter () { + # + # Verifies if $1 is not null, and $# = 1 + # +if test $# -eq 0 ; then + printerror syntax error: invalid empty parameter + exit 103 +elif test $# -ne 1 ; then + printerror syntax error: embedded blanks are not allowed in \"$*\" + exit 103 +fi +return +} + +## +## main +## + +## Verifies command line arguments + +if test $# -eq 0 ; then + printerror syntax error: missing required parameter, --help assumed + dohelp + exit 103 +fi + +state=I +while test $# -gt 0 && test ${state} != III ; do + case "$1" in + --help) dohelp + exit 0 + ;; + --quiet) BEQUIET=--quiet + ;; + --force) FORCE=yes + RETRY=yes + ;; + --try-anyway) + RETRY=yes + ;; + --disclose-deny) + RETURNFAILURE=yes + ;; + --query) MODE=query + RETURNFAILURE=yes + ;; + --no-fallback) + NOFALLBACK=yes + ;; + --skip-systemd-native) SKIP_SYSTEMD_NATIVE=yes + ;; + --*) printerror syntax error: unknown option \"$1\" + exit 103 + ;; + *) case ${state} in + I) verifyparameter $1 + INITSCRIPTID=$1 + ;; + II) verifyparameter $1 + ACTION=$1 + ;; + esac + state=${state}I + ;; + esac + shift +done + +if test ${state} != III ; then + printerror syntax error: missing required parameter + exit 103 +fi + +#NOTE: It may not be obvious, but "$@" from this point on must expand +#to the extra initscript parameters, except inside functions. + +if test -d /run/systemd/system ; then + is_systemd=1 + UNIT="${INITSCRIPTID%.sh}.service" +elif test -f /run/openrc/softlevel ; then + is_openrc=1 +elif test ! -f "${INITDPREFIX}${INITSCRIPTID}" ; then + ## Verifies if the given initscript ID is known + ## For sysvinit, this error is critical + printerror unknown initscript, ${INITDPREFIX}${INITSCRIPTID} not found. +fi + +## Queries sysvinit for the current runlevel +if [ ! -x ${RUNLEVELHELPER} ] || ! RL=`${RUNLEVELHELPER}`; then + if [ -n "$is_systemd" ] && systemctl is-active --quiet sysinit.target; then + # under systemd, the [2345] runlevels are only set upon reaching them; + # if we are past sysinit.target (roughly equivalent to rcS), consider + # this as runlevel 5 (this is only being used for validating rcN.d + # symlinks, so the precise value does not matter much) + RL=5 + else + printerror "could not determine current runlevel" + # this usually fails in schroots etc., ignore failure (#823611) + RL= + fi +fi +# strip off previous runlevel +RL=${RL#* } + +## Running ${RUNLEVELHELPER} to get current runlevel do not work in +## the boot runlevel (scripts in /etc/rcS.d/), as /var/run/utmp +## contains runlevel 0 or 6 (written at shutdown) at that point. +if test x${RL} = x0 || test x${RL} = x6 ; then + if ps -fp 1 | grep -q 'init boot' ; then + RL=S + fi +fi + +## Handles shutdown sequences VERY safely +## i.e.: forget about policy, and do all we can to run the script. +## BTW, why the heck are we being run in a shutdown runlevel?! +if test x${RL} = x0 || test x${RL} = x6 ; then + FORCE=yes + RETRY=yes + POLICYHELPER= + BEQUIET= + printerror "-----------------------------------------------------" + printerror "WARNING: 'invoke-rc.d ${INITSCRIPTID} ${ACTION}' called" + printerror "during shutdown sequence." + printerror "enabling safe mode: initscript policy layer disabled" + printerror "-----------------------------------------------------" +fi + +## Verifies the existance of proper S??initscriptID and K??initscriptID +## *links* in the proper /etc/rc?.d/ directory +verifyrclink () { + # + # verifies if parameters are non-dangling symlinks + # all parameters are verified + # + doexit= + while test $# -gt 0 ; do + if test ! -L "$1" ; then + printerror not a symlink: $1 + doexit=102 + fi + if test ! -f "$1" ; then + printerror dangling symlink: $1 + doexit=102 + fi + shift + done + if test x${doexit} != x && test x${RETRY} = x; then + exit ${doexit} + fi + return 0 +} + +testexec () { + # + # returns true if any of the parameters is + # executable (after following links) + # + while test $# -gt 0 ; do + if test -x "$1" ; then + return 0 + fi + shift + done + return 1 +} + +RC= + +### +### LOCAL POLICY: Enforce that the script/unit is enabled. For SysV init +### scripts, this needs a start entry in either runlevel S or current runlevel +### to allow start or restart. +if [ -n "$is_systemd" ]; then + case ${ACTION} in + start|restart|try-restart) + # If a package ships both init script and systemd service file, the + # systemd unit will not be enabled by the time invoke-rc.d is called + # (with current debhelper sequence). This would make systemctl is-enabled + # report the wrong status, and then the service would not be started. + # This check cannot be removed as long as we support not passing --skip-systemd-native + + if systemctl --quiet is-enabled "${UNIT}" 2>/dev/null || \ + ls ${RCDPREFIX}[S2345].d/S[0-9][0-9]${INITSCRIPTID} >/dev/null 2>&1; then + RC=104 + elif systemctl --quiet is-active "${UNIT}" 2>/dev/null; then + RC=104 + else + RC=101 + fi + ;; + esac +else + # we do handle multiple links per runlevel + # but we don't handle embedded blanks in link names :-( + if test x${RL} != x ; then + SLINK=`ls -d -Q ${RCDPREFIX}${RL}.d/S[0-9][0-9]${INITSCRIPTID} 2>/dev/null | xargs` + KLINK=`ls -d -Q ${RCDPREFIX}${RL}.d/K[0-9][0-9]${INITSCRIPTID} 2>/dev/null | xargs` + SSLINK=`ls -d -Q ${RCDPREFIX}S.d/S[0-9][0-9]${INITSCRIPTID} 2>/dev/null | xargs` + + verifyrclink ${SLINK} ${KLINK} ${SSLINK} + fi + + case ${ACTION} in + start|restart|try-restart) + if testexec ${SLINK} ; then + RC=104 + elif testexec ${KLINK} ; then + RC=101 + elif testexec ${SSLINK} ; then + RC=104 + else + RC=101 + fi + ;; + esac +fi + +# test if /etc/init.d/initscript is actually executable +_executable= +if [ -n "$is_systemd" ]; then + _executable=1 +elif testexec "${INITDPREFIX}${INITSCRIPTID}"; then + _executable=1 +fi +if [ "$_executable" = "1" ]; then + if test x${RC} = x && test x${MODE} = xquery ; then + RC=105 + fi + + # call policy layer + querypolicy + case ${RC} in + 101|104) + ;; + *) if test x${MODE} != xquery ; then + printerror policy-rc.d returned error status ${RC} + if test x${RETRY} = x ; then + exit ${RC} + else + RC=102 + fi + fi + ;; + esac +else + ### + ### LOCAL INITSCRIPT POLICY: non-executable initscript; deny exec. + ### (this is common sense, actually :^P ) + ### + RC=101 +fi + +## Handles --query +if test x${MODE} = xquery ; then + exit ${RC} +fi + + +setechoactions () { + if test $# -gt 1 ; then + echoaction=true + else + echoaction= + fi +} +getnextaction () { + saction=$1 + shift + ACTION="$@" +} + +## Executes initscript +## note that $ACTION is a space-separated list of actions +## to be attempted in order until one suceeds. +if test x${FORCE} != x || test ${RC} -eq 104 ; then + if [ -n "$is_systemd" ] || testexec "${INITDPREFIX}${INITSCRIPTID}" ; then + RC=102 + setechoactions ${ACTION} + while test ! -z "${ACTION}" ; do + getnextaction ${ACTION} + if test ! -z ${echoaction} ; then + printerror executing initscript action \"${saction}\"... + fi + + if [ -n "$is_systemd" ]; then + if [ "$SKIP_SYSTEMD_NATIVE" = yes ] ; then + case $(systemctl show --value --property SourcePath "${UNIT}") in + /etc/init.d/*) + ;; + *) + # We were asked to skip native systemd units, and this one was not generated by the sysv generator + # exit cleanly + exit 0 + ;; + esac + + fi + _state=$(systemctl -p LoadState show "${UNIT}" 2>/dev/null) + + case $saction in + start|restart|try-restart) + [ "$_state" != "LoadState=masked" ] || exit 0 + systemctl $sctl_args "${saction}" "${UNIT}" && exit 0 + ;; + stop|status) + systemctl $sctl_args "${saction}" "${UNIT}" && exit 0 + ;; + reload) + [ "$_state" != "LoadState=masked" ] || exit 0 + _canreload="$(systemctl -p CanReload show ${UNIT} 2>/dev/null)" + # Don't block on reload requests during bootup and shutdown + # from units/hooks and simply schedule the task. + if ! systemctl --quiet is-system-running; then + sctl_args="--no-block" + fi + if [ "$_canreload" = "CanReload=no" ]; then + "${INITDPREFIX}${INITSCRIPTID}" "${saction}" "$@" && exit 0 + else + systemctl $sctl_args reload "${UNIT}" && exit 0 + fi + ;; + force-stop) + systemctl --signal=KILL kill "${UNIT}" && exit 0 + ;; + force-reload) + [ "$_state" != "LoadState=masked" ] || exit 0 + _canreload="$(systemctl -p CanReload show ${UNIT} 2>/dev/null)" + if [ "$_canreload" = "CanReload=no" ]; then + systemctl $sctl_args restart "${UNIT}" && exit 0 + else + systemctl $sctl_args reload "${UNIT}" && exit 0 + fi + ;; + *) + # We try to run non-standard actions by running + # the init script directly. + "${INITDPREFIX}${INITSCRIPTID}" "${saction}" "$@" && exit 0 + ;; + esac + elif [ -n "$is_openrc" ]; then + rc-service "${INITSCRIPTID}" "${saction}" && exit 0 + else + "${INITDPREFIX}${INITSCRIPTID}" "${saction}" "$@" && exit 0 + fi + RC=$? + + if test ! -z "${ACTION}" ; then + printerror action \"${saction}\" failed, trying next action... + fi + done + printerror initscript ${INITSCRIPTID}, action \"${saction}\" failed. + if [ -n "$is_systemd" ] && [ "$saction" = start -o "$saction" = restart -o "$saction" = "try-restart" ]; then + systemctl status --full --no-pager "${UNIT}" || true + fi + exit ${RC} + fi + exit 102 +fi + +## Handles --disclose-deny and denied "status" action (bug #381497) +if test ${RC} -eq 101 && test x${RETURNFAILURE} = x ; then + if test "x${ACTION%% *}" = "xstatus"; then + printerror emulating initscript action \"status\", returning \"unknown\" + RC=4 + else + RC=0 + fi +else + formataction ${ACTION} + printerror initscript ${naction} \"${printaction}\" not executed. +fi + +exit ${RC} diff --git a/script/service b/script/service new file mode 100755 index 0000000..08f69bb --- /dev/null +++ b/script/service @@ -0,0 +1,217 @@ +#!/bin/sh + +########################################################################### +# /usr/bin/service +# +# A convenient wrapper for the /etc/init.d init scripts. +# +# This script is a modified version of the /sbin/service utility found on +# Red Hat/Fedora systems (licensed GPLv2+). +# +# Copyright (C) 2006 Red Hat, Inc. All rights reserved. +# Copyright (C) 2008 Canonical Ltd. +# * August 2008 - Dustin Kirkland <kirkland@canonical.com> +# Copyright (C) 2013 Michael Stapelberg <stapelberg@debian.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# On Debian GNU/Linux systems, the complete text of the GNU General +# Public License can be found in `/usr/share/common-licenses/GPL-2'. +########################################################################### + + +is_ignored_file() { + case "$1" in + skeleton | README | *.dpkg-dist | *.dpkg-old | rc | rcS | single | reboot | bootclean.sh) + return 0 + ;; + esac + return 1 +} + +VERSION="`basename $0` ver. __VERSION__" +USAGE="Usage: `basename $0` < option > | --status-all | \ +[ service_name [ command | --full-restart ] ]" +SERVICE= +ACTION= +SERVICEDIR="/etc/init.d" +OPTIONS= +is_systemd= + + +if [ $# -eq 0 ]; then + echo "${USAGE}" >&2 + exit 1 +fi + +if [ -d /run/systemd/system ]; then + is_systemd=1 +fi + +cd / +while [ $# -gt 0 ]; do + case "${1}" in + --help | -h | --h* ) + echo "${USAGE}" >&2 + exit 0 + ;; + --version | -V ) + echo "${VERSION}" >&2 + exit 0 + ;; + *) + if [ -z "${SERVICE}" -a $# -eq 1 -a "${1}" = "--status-all" ]; then + cd ${SERVICEDIR} + for SERVICE in * ; do + case "${SERVICE}" in + functions | halt | killall | single| linuxconf| kudzu) + ;; + *) + if ! is_ignored_file "${SERVICE}" \ + && [ -x "${SERVICEDIR}/${SERVICE}" ]; then + out=$(env -i LANG="$LANG" LANGUAGE="$LANGUAGE" LC_CTYPE="$LC_CTYPE" LC_NUMERIC="$LC_NUMERIC" LC_TIME="$LC_TIME" LC_COLLATE="$LC_COLLATE" LC_MONETARY="$LC_MONETARY" LC_MESSAGES="$LC_MESSAGES" LC_PAPER="$LC_PAPER" LC_NAME="$LC_NAME" LC_ADDRESS="$LC_ADDRESS" LC_TELEPHONE="$LC_TELEPHONE" LC_MEASUREMENT="$LC_MEASUREMENT" LC_IDENTIFICATION="$LC_IDENTIFICATION" LC_ALL="$LC_ALL" PATH="$PATH" TERM="$TERM" "$SERVICEDIR/$SERVICE" status 2>&1) + retval=$? + if echo "$out" | grep -Fiq "usage:"; then + #printf " %s %-60s %s\n" "[?]" "$SERVICE:" "unknown" 1>&2 + echo " [ ? ] $SERVICE" 1>&2 + continue + else + if [ "$retval" = "0" -a -n "$out" ]; then + #printf " %s %-60s %s\n" "[+]" "$SERVICE:" "running" + echo " [ + ] $SERVICE" + continue + else + #printf " %s %-60s %s\n" "[-]" "$SERVICE:" "NOT running" + echo " [ - ] $SERVICE" + continue + fi + fi + #env -i LANG="$LANG" LANGUAGE="$LANGUAGE" LC_CTYPE="$LC_CTYPE" LC_NUMERIC="$LC_NUMERIC" LC_TIME="$LC_TIME" LC_COLLATE="$LC_COLLATE" LC_MONETARY="$LC_MONETARY" LC_MESSAGES="$LC_MESSAGES" LC_PAPER="$LC_PAPER" LC_NAME="$LC_NAME" LC_ADDRESS="$LC_ADDRESS" LC_TELEPHONE="$LC_TELEPHONE" LC_MEASUREMENT="$LC_MEASUREMENT" LC_IDENTIFICATION="$LC_IDENTIFICATION" LC_ALL="$LC_ALL" PATH="$PATH" TERM="$TERM" "$SERVICEDIR/$SERVICE" status + fi + ;; + esac + done + exit 0 + elif [ $# -eq 2 -a "${2}" = "--full-restart" ]; then + SERVICE="${1}" + # On systems using systemd, we just perform a normal restart: + # A restart with systemd is already a full restart. + if [ -n "$is_systemd" ]; then + ACTION="restart" + else + if [ -x "${SERVICEDIR}/${SERVICE}" ]; then + env -i LANG="$LANG" LANGUAGE="$LANGUAGE" LC_CTYPE="$LC_CTYPE" LC_NUMERIC="$LC_NUMERIC" LC_TIME="$LC_TIME" LC_COLLATE="$LC_COLLATE" LC_MONETARY="$LC_MONETARY" LC_MESSAGES="$LC_MESSAGES" LC_PAPER="$LC_PAPER" LC_NAME="$LC_NAME" LC_ADDRESS="$LC_ADDRESS" LC_TELEPHONE="$LC_TELEPHONE" LC_MEASUREMENT="$LC_MEASUREMENT" LC_IDENTIFICATION="$LC_IDENTIFICATION" LC_ALL="$LC_ALL" PATH="$PATH" TERM="$TERM" "$SERVICEDIR/$SERVICE" stop + env -i LANG="$LANG" LANGUAGE="$LANGUAGE" LC_CTYPE="$LC_CTYPE" LC_NUMERIC="$LC_NUMERIC" LC_TIME="$LC_TIME" LC_COLLATE="$LC_COLLATE" LC_MONETARY="$LC_MONETARY" LC_MESSAGES="$LC_MESSAGES" LC_PAPER="$LC_PAPER" LC_NAME="$LC_NAME" LC_ADDRESS="$LC_ADDRESS" LC_TELEPHONE="$LC_TELEPHONE" LC_MEASUREMENT="$LC_MEASUREMENT" LC_IDENTIFICATION="$LC_IDENTIFICATION" LC_ALL="$LC_ALL" PATH="$PATH" TERM="$TERM" "$SERVICEDIR/$SERVICE" start + exit $? + fi + fi + elif [ -z "${SERVICE}" ]; then + SERVICE="${1}" + elif [ -z "${ACTION}" ]; then + ACTION="${1}" + else + OPTIONS="${OPTIONS} ${1}" + fi + shift + ;; + esac +done + +run_via_sysvinit() { + # Otherwise, use the traditional sysvinit + if [ -x "${SERVICEDIR}/${SERVICE}" ]; then + exec env -i LANG="$LANG" LANGUAGE="$LANGUAGE" LC_CTYPE="$LC_CTYPE" LC_NUMERIC="$LC_NUMERIC" LC_TIME="$LC_TIME" LC_COLLATE="$LC_COLLATE" LC_MONETARY="$LC_MONETARY" LC_MESSAGES="$LC_MESSAGES" LC_PAPER="$LC_PAPER" LC_NAME="$LC_NAME" LC_ADDRESS="$LC_ADDRESS" LC_TELEPHONE="$LC_TELEPHONE" LC_MEASUREMENT="$LC_MEASUREMENT" LC_IDENTIFICATION="$LC_IDENTIFICATION" LC_ALL="$LC_ALL" PATH="$PATH" TERM="$TERM" "$SERVICEDIR/$SERVICE" ${ACTION} ${OPTIONS} + else + echo "${SERVICE}: unrecognized service" >&2 + exit 1 + fi +} + +update_openrc_started_symlinks() { + # maintain the symlinks of /run/openrc/started so that + # rc-status works with the service command as well + if [ -d /run/openrc/started ] ; then + case "${ACTION}" in + start) + if [ ! -h /run/openrc/started/$SERVICE ] ; then + ln -s $SERVICEDIR/$SERVICE /run/openrc/started/$SERVICE || true + fi + ;; + stop) + rm /run/openrc/started/$SERVICE || true + ;; + esac + fi +} + +# When this machine is running systemd, standard service calls are turned into +# systemctl calls. +if [ -n "$is_systemd" ] +then + UNIT="${SERVICE%.sh}.service" + + case "${ACTION}" in + restart|status|try-restart) + exec systemctl $sctl_args ${ACTION} ${UNIT} + ;; + start|stop) + # Follow the principle of least surprise for SysV people: + # When running "service foo stop" and foo happens to be a service that + # has one or more .socket files, we also stop the .socket units. + # Users who need more control will use systemctl directly. + for unit in $(systemctl list-unit-files --full --type=socket 2>/dev/null | sed -ne 's/\.socket\s*[a-z]*\s*$/.socket/p'); do + if [ "$(systemctl -p Triggers show $unit)" = "Triggers=${UNIT}" ]; then + systemctl $sctl_args ${ACTION} $unit + fi + done + exec systemctl $sctl_args ${ACTION} ${UNIT} + ;; + reload) + _canreload="$(systemctl -p CanReload show ${UNIT} 2>/dev/null)" + # Don't block on reload requests during bootup and shutdown + # from units/hooks and simply schedule the task. + if ! systemctl --quiet is-system-running; then + sctl_args="--no-block" + fi + if [ "$_canreload" = "CanReload=no" ]; then + # The reload action falls back to the sysv init script just in case + # the systemd service file does not (yet) support reload for a + # specific service. + run_via_sysvinit + else + exec systemctl $sctl_args reload "${UNIT}" + fi + ;; + force-stop) + exec systemctl --signal=KILL kill "${UNIT}" + ;; + force-reload) + _canreload="$(systemctl -p CanReload show ${UNIT} 2>/dev/null)" + if [ "$_canreload" = "CanReload=no" ]; then + exec systemctl $sctl_args restart "${UNIT}" + else + exec systemctl $sctl_args reload "${UNIT}" + fi + ;; + *) + # We try to run non-standard actions by running + # the init script directly. + run_via_sysvinit + ;; + esac +fi + +update_openrc_started_symlinks +run_via_sysvinit diff --git a/script/update-rc.d b/script/update-rc.d new file mode 100755 index 0000000..264757b --- /dev/null +++ b/script/update-rc.d @@ -0,0 +1,543 @@ +#! /usr/bin/perl +# vim: ft=perl +# +# update-rc.d Update the links in /etc/rc[0-9S].d/ +# + +use strict; +use warnings; +# NB: All Perl modules used here must be in perl-base. Specifically, depending +# on modules in perl-modules is not okay! See bug #716923 + +my $initd = "/etc/init.d"; +my $etcd = "/etc/rc"; +my $dpkg_root = $ENV{DPKG_ROOT} // ''; + +# Print usage message and die. + +sub usage { + print STDERR "update-rc.d: error: @_\n" if ($#_ >= 0); + print STDERR <<EOF; +usage: update-rc.d [-f] <basename> remove + update-rc.d [-f] <basename> defaults + update-rc.d [-f] <basename> defaults-disabled + update-rc.d <basename> disable|enable [S|2|3|4|5] + -f: force + +The disable|enable API is not stable and might change in the future. +EOF + exit (1); +} + +exit main(@ARGV); + +sub info { + print STDOUT "update-rc.d: @_\n"; +} + +sub warning { + print STDERR "update-rc.d: warning: @_\n"; +} + +sub error { + print STDERR "update-rc.d: error: @_\n"; + exit (1); +} + +sub error_code { + my $rc = shift; + print STDERR "update-rc.d: error: @_\n"; + exit ($rc); +} + +sub make_path { + my ($path) = @_; + my @dirs = (); + my @path = split /\//, $path; + map { push @dirs, $_; mkdir join('/', @dirs), 0755; } @path; +} + +# Given a script name, return any runlevels except 0 or 6 in which the +# script is enabled. If that gives nothing and the script is not +# explicitly disabled, return 6 if the script is disabled in runlevel +# 0 or 6. +sub script_runlevels { + my ($scriptname) = @_; + my @links=<"$dpkg_root/etc/rc[S12345].d/S[0-9][0-9]$scriptname">; + if (@links) { + return map(substr($_, 7, 1), @links); + } elsif (! <"$dpkg_root/etc/rc[S12345].d/K[0-9][0-9]$scriptname">) { + @links=<"$dpkg_root/etc/rc[06].d/K[0-9][0-9]$scriptname">; + return ("6") if (@links); + } else { + return ; + } +} + +# Map the sysvinit runlevel to that of openrc. +sub openrc_rlconv { + my %rl_table = ( + "S" => "sysinit", + "1" => "recovery", + "2" => "default", + "3" => "default", + "4" => "default", + "5" => "default", + "6" => "off" ); + + my %seen; # return unique runlevels + return grep !$seen{$_}++, map($rl_table{$_}, @_); +} + +sub systemd_reload { + if (length $ENV{DPKG_ROOT}) { + # if we operate on a chroot from the outside, do not attempt to reload + return; + } + if (-d "/run/systemd/system") { + system("systemctl", "daemon-reload"); + } +} + +# Creates the necessary links to enable/disable a SysV init script (fallback if +# no insserv/rc-update exists) +sub make_sysv_links { + my ($scriptname, $action) = @_; + + # for "remove" we cannot rely on the init script still being present, as + # this gets called in postrm for purging. Just remove all symlinks. + if ("remove" eq $action) { unlink($_) for + glob("$dpkg_root/etc/rc?.d/[SK][0-9][0-9]$scriptname"); return; } + + # if the service already has any links, do not touch them + # numbers we don't care about, but enabled/disabled state we do + return if glob("$dpkg_root/etc/rc?.d/[SK][0-9][0-9]$scriptname"); + + # for "defaults", parse Default-{Start,Stop} and create these links + my ($lsb_start_ref, $lsb_stop_ref) = parse_def_start_stop("$dpkg_root/etc/init.d/$scriptname"); + my $start = $action eq "defaults-disabled" ? "K" : "S"; + foreach my $lvl (@$lsb_start_ref) { + make_path("$dpkg_root/etc/rc$lvl.d"); + my $l = "$dpkg_root/etc/rc$lvl.d/${start}01$scriptname"; + symlink("../init.d/$scriptname", $l); + } + + foreach my $lvl (@$lsb_stop_ref) { + make_path("$dpkg_root/etc/rc$lvl.d"); + my $l = "$dpkg_root/etc/rc$lvl.d/K01$scriptname"; + symlink("../init.d/$scriptname", $l); + } +} + +# Creates the necessary links to enable/disable the service (equivalent of an +# initscript) in systemd. +sub make_systemd_links { + my ($scriptname, $action) = @_; + + # If called by systemctl (via systemd-sysv-install), do nothing to avoid + # an endless loop. + if (defined($ENV{_SKIP_SYSTEMD_NATIVE}) && $ENV{_SKIP_SYSTEMD_NATIVE} == 1) { + return; + } + + # If systemctl is available, let's use that to create the symlinks. + if (-x "$dpkg_root/bin/systemctl" || -x "$dpkg_root/usr/bin/systemctl") { + my $systemd_root = '/'; + if ($dpkg_root ne '') { + $systemd_root = $dpkg_root; + } + # Set this env var to avoid loop in systemd-sysv-install. + local $ENV{SYSTEMCTL_SKIP_SYSV} = 1; + # Use --quiet to mimic the old update-rc.d behaviour. + system("systemctl", "--root=$systemd_root", "--quiet", "$action", "$scriptname"); + return; + } + + # In addition to the insserv call we also enable/disable the service + # for systemd by creating the appropriate symlink in case there is a + # native systemd service. In case systemd is not installed we do this + # on our own instead of using systemctl. + my $service_path; + if (-f "/etc/systemd/system/$scriptname.service") { + $service_path = "/etc/systemd/system/$scriptname.service"; + } elsif (-f "/lib/systemd/system/$scriptname.service") { + $service_path = "/lib/systemd/system/$scriptname.service"; + } elsif (-f "/usr/lib/systemd/system/$scriptname.service") { + $service_path = "/usr/lib/systemd/system/$scriptname.service"; + } + if (defined($service_path)) { + my $changed_sth; + open my $fh, '<', $service_path or error("unable to read $service_path"); + while (<$fh>) { + chomp; + if (/^\s*WantedBy=(.+)$/i) { + my $wants_dir = "/etc/systemd/system/$1.wants"; + my $service_link = "$wants_dir/$scriptname.service"; + if ("enable" eq $action) { + make_path($wants_dir); + symlink($service_path, $service_link); + } else { + unlink($service_link) if -e $service_link; + } + } + } + close($fh); + } +} + +sub create_sequence { + my $force = (@_); + my $insserv = "$dpkg_root/usr/lib/insserv/insserv"; + # Fallback for older insserv package versions [2014-04-16] + $insserv = "/sbin/insserv" if ( -x "$dpkg_root/sbin/insserv"); + # If insserv is not configured it is not fully installed + my $insserv_installed = -x "$dpkg_root$insserv" && -e "$dpkg_root/etc/insserv.conf"; + my @opts; + push(@opts, '-f') if $force; + # Add force flag if initscripts is not installed + # This enables inistcripts-less systems to not fail when a facility is missing + unshift(@opts, '-f') unless is_initscripts_installed(); + if ( $dpkg_root ne '' ) { + push( @opts, + '--path', "$dpkg_root/etc/init.d", + '--override', "$dpkg_root/etc/insserv/overrides/", + '--insserv-dir', "$dpkg_root/etc/init.d", + '--config', "$dpkg_root/etc/insserv.conf" ); + } + + my $openrc_installed = -x "$dpkg_root/sbin/openrc"; + + my $sysv_insserv ={}; + $sysv_insserv->{remove} = sub { + my ($scriptname) = @_; + if ( -f "$dpkg_root/etc/init.d/$scriptname" ) { + return system($insserv, @opts, "-r", $scriptname) >> 8; + } else { + # insserv removes all dangling symlinks, no need to tell it + # what to look for. + my $rc = system($insserv, @opts) >> 8; + error_code($rc, "insserv rejected the script header") if $rc; + } + }; + $sysv_insserv->{defaults} = sub { + my ($scriptname) = @_; + if ( -f "$dpkg_root/etc/init.d/$scriptname" ) { + my $rc = system($insserv, @opts, $scriptname) >> 8; + error_code($rc, "insserv rejected the script header") if $rc; + } else { + error("initscript does not exist: /etc/init.d/$scriptname"); + } + }; + $sysv_insserv->{defaults_disabled} = sub { + my ($scriptname) = @_; + return if glob("$dpkg_root/etc/rc?.d/[SK][0-9][0-9]$scriptname"); + if ( -f "$dpkg_root/etc/init.d/$scriptname" ) { + my $rc = system($insserv, @opts, $scriptname) >> 8; + error_code($rc, "insserv rejected the script header") if $rc; + } else { + error("initscript does not exist: /etc/init.d/$scriptname"); + } + sysv_toggle("disable", $scriptname); + }; + $sysv_insserv->{toggle} = sub { + my ($action, $scriptname) = (shift, shift); + sysv_toggle($action, $scriptname, @_); + + # Call insserv to resequence modified links + my $rc = system($insserv, @opts, $scriptname) >> 8; + error_code($rc, "insserv rejected the script header") if $rc; + }; + + my $sysv_plain = {}; + $sysv_plain->{remove} = sub { + my ($scriptname) = @_; + make_sysv_links($scriptname, "remove"); + }; + $sysv_plain->{defaults} = sub { + my ($scriptname) = @_; + make_sysv_links($scriptname, "defaults"); + }; + $sysv_plain->{defaults_disabled} = sub { + my ($scriptname) = @_; + make_sysv_links($scriptname, "defaults-disabled"); + }; + $sysv_plain->{toggle} = sub { + my ($action, $scriptname) = (shift, shift); + sysv_toggle($action, $scriptname, @_); + }; + + my $systemd = {}; + $systemd->{remove} = sub { + systemd_reload; + }; + $systemd->{defaults} = sub { + systemd_reload; + }; + $systemd->{defaults_disabled} = sub { + systemd_reload; + }; + $systemd->{toggle} = sub { + my ($action, $scriptname) = (shift, shift); + make_systemd_links($scriptname, $action); + systemd_reload; + }; + + # Should we check exit codeS? + my $openrc = {}; + $openrc->{remove} = sub { + my ($scriptname) = @_; + system("rc-update", "-qqa", "delete", $scriptname); + + }; + $openrc->{defaults} = sub { + my ($scriptname) = @_; + # OpenRC does not distinguish halt and reboot. They are handled + # by /etc/init.d/transit instead. + return if ("halt" eq $scriptname || "reboot" eq $scriptname); + # no need to consider default disabled runlevels + # because everything is disabled by openrc by default + my @rls=script_runlevels($scriptname); + if ( @rls ) { + system("rc-update", "add", $scriptname, openrc_rlconv(@rls)); + } + }; + $openrc->{defaults_disabled} = sub { + # In openrc everything is disabled by default + }; + $openrc->{toggle} = sub { + my ($action, $scriptname) = (shift, shift); + my (@toggle_lvls, $start_lvls, $stop_lvls, @symlinks); + my $lsb_header = lsb_header_for_script($scriptname); + + # Extra arguments to disable|enable action are runlevels. If none + # given parse LSB info for Default-Start value. + if ($#_ >= 0) { + @toggle_lvls = @_; + } else { + ($start_lvls, $stop_lvls) = parse_def_start_stop($lsb_header); + @toggle_lvls = @$start_lvls; + if ($#toggle_lvls < 0) { + error("$scriptname Default-Start contains no runlevels, aborting."); + } + } + my %openrc_act = ( "disable" => "del", "enable" => "add" ); + system("rc-update", $openrc_act{$action}, $scriptname, + openrc_rlconv(@toggle_lvls)) + }; + + my @sequence; + if ($insserv_installed) { + push @sequence, $sysv_insserv; + } + else { + push @sequence, $sysv_plain; + } + # OpenRC has to be after sysv_{insserv,plain} because it depends on them to synchronize + # states. + if ($openrc_installed) { + push @sequence, $openrc; + } + push @sequence, $systemd; + + return @sequence; +} + +## Dependency based +sub main { + my @args = @_; + my $scriptname; + my $action; + my $force = 0; + + while($#args >= 0 && ($_ = $args[0]) =~ /^-/) { + shift @args; + if (/^-f$/) { $force = 1; next } + if (/^-h|--help$/) { usage(); } + usage("unknown option"); + } + + usage("not enough arguments") if ($#args < 1); + + my @sequence = create_sequence($force); + + $scriptname = shift @args; + $action = shift @args; + if ("remove" eq $action) { + foreach my $init (@sequence) { + $init->{remove}->($scriptname); + } + } elsif ("defaults" eq $action || "start" eq $action || + "stop" eq $action) { + # All start/stop/defaults arguments are discarded so emit a + # message if arguments have been given and are in conflict + # with Default-Start/Default-Stop values of LSB comment. + if ("start" eq $action || "stop" eq $action) { + cmp_args_with_defaults($scriptname, $action, @args); + } + foreach my $init (@sequence) { + $init->{defaults}->($scriptname); + } + } elsif ("defaults-disabled" eq $action) { + foreach my $init (@sequence) { + $init->{defaults_disabled}->($scriptname); + } + } elsif ("disable" eq $action || "enable" eq $action) { + foreach my $init (@sequence) { + $init->{toggle}->($action, $scriptname, @args); + } + } else { + usage(); + } +} + +sub parse_def_start_stop { + my $script = shift; + my (%lsb, @def_start_lvls, @def_stop_lvls); + + open my $fh, '<', $script or error("unable to read $script"); + while (<$fh>) { + chomp; + if (m/^### BEGIN INIT INFO\s*$/) { + $lsb{'begin'}++; + } + elsif (m/^### END INIT INFO\s*$/) { + $lsb{'end'}++; + last; + } + elsif ($lsb{'begin'} and not $lsb{'end'}) { + if (m/^# Default-Start:\s*(\S?.*)$/) { + @def_start_lvls = split(' ', $1); + } + if (m/^# Default-Stop:\s*(\S?.*)$/) { + @def_stop_lvls = split(' ', $1); + } + } + } + close($fh); + + return (\@def_start_lvls, \@def_stop_lvls); +} + +sub lsb_header_for_script { + my $name = shift; + + foreach my $file ("/etc/insserv/overrides/$name", "/etc/init.d/$name", + "/usr/share/insserv/overrides/$name") { + return $file if -s $file; + } + + error("cannot find a LSB script for $name"); +} + +sub cmp_args_with_defaults { + my ($name, $act) = (shift, shift); + my ($lsb_start_ref, $lsb_stop_ref, $arg_str, $lsb_str); + my (@arg_start_lvls, @arg_stop_lvls, @lsb_start_lvls, @lsb_stop_lvls); + + ($lsb_start_ref, $lsb_stop_ref) = parse_def_start_stop("/etc/init.d/$name"); + @lsb_start_lvls = @$lsb_start_ref; + @lsb_stop_lvls = @$lsb_stop_ref; + return if (!@lsb_start_lvls and !@lsb_stop_lvls); + + warning "start and stop actions are no longer supported; falling back to defaults"; + my $start = $act eq 'start' ? 1 : 0; + my $stop = $act eq 'stop' ? 1 : 0; + + # The legacy part of this program passes arguments starting with + # "start|stop NN x y z ." but the insserv part gives argument list + # starting with sequence number (ie. strips off leading "start|stop") + # Start processing arguments immediately after the first seq number. + my $argi = $_[0] eq $act ? 2 : 1; + + while (defined $_[$argi]) { + my $arg = $_[$argi]; + + # Runlevels 0 and 6 are always stop runlevels + if ($arg eq 0 or $arg eq 6) { + $start = 0; $stop = 1; + } elsif ($arg eq 'start') { + $start = 1; $stop = 0; $argi++; next; + } elsif ($arg eq 'stop') { + $start = 0; $stop = 1; $argi++; next; + } elsif ($arg eq '.') { + next; + } + push(@arg_start_lvls, $arg) if $start; + push(@arg_stop_lvls, $arg) if $stop; + } continue { + $argi++; + } + + if ($#arg_start_lvls != $#lsb_start_lvls or + join("\0", sort @arg_start_lvls) ne join("\0", sort @lsb_start_lvls)) { + $arg_str = @arg_start_lvls ? "@arg_start_lvls" : "none"; + $lsb_str = @lsb_start_lvls ? "@lsb_start_lvls" : "none"; + warning "start runlevel arguments ($arg_str) do not match", + "$name Default-Start values ($lsb_str)"; + } + if ($#arg_stop_lvls != $#lsb_stop_lvls or + join("\0", sort @arg_stop_lvls) ne join("\0", sort @lsb_stop_lvls)) { + $arg_str = @arg_stop_lvls ? "@arg_stop_lvls" : "none"; + $lsb_str = @lsb_stop_lvls ? "@lsb_stop_lvls" : "none"; + warning "stop runlevel arguments ($arg_str) do not match", + "$name Default-Stop values ($lsb_str)"; + } +} + +sub sysv_toggle { + my ($act, $name) = (shift, shift); + my (@toggle_lvls, $start_lvls, $stop_lvls, @symlinks); + my $lsb_header = lsb_header_for_script($name); + + # Extra arguments to disable|enable action are runlevels. If none + # given parse LSB info for Default-Start value. + if ($#_ >= 0) { + @toggle_lvls = @_; + } else { + ($start_lvls, $stop_lvls) = parse_def_start_stop($lsb_header); + @toggle_lvls = @$start_lvls; + if ($#toggle_lvls < 0) { + error("$name Default-Start contains no runlevels, aborting."); + } + } + + # Find symlinks in rc.d directories. Refuse to modify links in runlevels + # not used for normal system start sequence. + for my $lvl (@toggle_lvls) { + if ($lvl !~ /^[S2345]$/) { + warning("$act action will have no effect on runlevel $lvl"); + next; + } + push(@symlinks, $_) for glob("$dpkg_root/etc/rc$lvl.d/[SK][0-9][0-9]$name"); + } + + if (!@symlinks) { + error("no runlevel symlinks to modify, aborting!"); + } + + # Toggle S/K bit of script symlink. + for my $cur_lnk (@symlinks) { + my $sk; + my @new_lnk = split(//, $cur_lnk); + + if ("disable" eq $act) { + $sk = rindex($cur_lnk, '/S') + 1; + next if $sk < 1; + $new_lnk[$sk] = 'K'; + } else { + $sk = rindex($cur_lnk, '/K') + 1; + next if $sk < 1; + $new_lnk[$sk] = 'S'; + } + + rename($cur_lnk, join('', @new_lnk)) or error($!); + } +} + +# Try to determine if initscripts is installed +sub is_initscripts_installed { + # Check if mountkernfs is available. We cannot make inferences + # using the running init system because we may be running in a + # chroot + return glob("$dpkg_root/etc/rcS.d/S??mountkernfs.sh"); +} diff --git a/t/001-deb-systemd-helper.t b/t/001-deb-systemd-helper.t new file mode 100644 index 0000000..c78ad53 --- /dev/null +++ b/t/001-deb-systemd-helper.t @@ -0,0 +1,473 @@ +#!perl +# vim:ts=4:sw=4:et + +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempfile tempdir); # in core since perl 5.6.1 +use File::Path qw(make_path); # in core since Perl 5.001 +use File::Basename; # in core since Perl 5 +use FindBin; # in core since Perl 5.00307 + +use lib "$FindBin::Bin/."; +use helpers; + +test_setup(); + +my $dpkg_root = $ENV{DPKG_ROOT} // ''; + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “is-enabled” is not true for a random, non-existing unit file. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my ($fh, $random_unit) = tempfile('unit\x2dXXXXX', + SUFFIX => '.service', + TMPDIR => 1, + UNLINK => 1); +close($fh); +$random_unit = basename($random_unit); + +isnt_enabled($random_unit); +isnt_debian_installed($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “is-enabled” is not true for a random, existing unit file. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my $servicefile_path = "$dpkg_root/lib/systemd/system/$random_unit"; +make_path("$dpkg_root/lib/systemd/system"); +open($fh, '>', $servicefile_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=multi-user.target +EOT +close($fh); + +isnt_enabled($random_unit); +isnt_debian_installed($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “enable” creates the requested symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +unless ($ENV{'TEST_ON_REAL_SYSTEM'}) { + # This might exist if we don't start from a fresh directory + ok(! -d "$dpkg_root/etc/systemd/system/multi-user.target.wants", + 'multi-user.target.wants does not exist yet'); +} + +my $retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); +my $symlink_path = "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit"; +ok(-l $symlink_path, "$random_unit was enabled"); +is($dpkg_root . readlink($symlink_path), $servicefile_path, + "symlink points to $servicefile_path"); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “is-enabled” now returns true. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +is_enabled($random_unit); +is_debian_installed($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify deleting the symlinks and running “enable” again does not ┃ +# ┃ re-create the symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +unlink($symlink_path); +ok(! -l $symlink_path, 'symlink deleted'); +isnt_enabled($random_unit); +is_debian_installed($random_unit); + +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); + +isnt_enabled($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “disable” when purging deletes the statefile. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my $statefile = "$dpkg_root/var/lib/systemd/deb-systemd-helper-enabled/$random_unit.dsh-also"; + +ok(-f $statefile, 'state file exists'); + +$ENV{'_DEB_SYSTEMD_HELPER_PURGE'} = '1'; +$retval = dsh('disable', $random_unit); +delete $ENV{'_DEB_SYSTEMD_HELPER_PURGE'}; +is($retval, 0, "disable command succeeded"); +ok(! -f $statefile, 'state file does not exist anymore after purging'); +isnt_debian_installed($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “enable” after purging does re-create the symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +ok(! -l $symlink_path, 'symlink does not exist yet'); +isnt_enabled($random_unit); + +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); + +is_enabled($random_unit); +is_debian_installed($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “disable” removes the symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +$ENV{'_DEB_SYSTEMD_HELPER_PURGE'} = '1'; +$retval = dsh('disable', $random_unit); +delete $ENV{'_DEB_SYSTEMD_HELPER_PURGE'}; +is($retval, 0, "disable command succeeded"); + +isnt_enabled($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “enable” after purging does re-create the symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +ok(! -l $symlink_path, 'symlink does not exist yet'); +isnt_enabled($random_unit); + +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); + +is_enabled($random_unit); +is_debian_installed($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify the “purge” verb works. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +$retval = dsh('purge', $random_unit); +is($retval, 0, "purge command succeeded"); + +isnt_enabled($random_unit); +isnt_debian_installed($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “enable” after purging does re-create the symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +ok(! -l $symlink_path, 'symlink does not exist yet'); +isnt_enabled($random_unit); + +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); + +is_enabled($random_unit); +is_debian_installed($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “mask” (when enabled) results in the symlink pointing to /dev/null ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my $mask_path = "$dpkg_root/etc/systemd/system/$random_unit"; +ok(! -l $mask_path, 'mask link does not exist yet'); + +$retval = dsh('mask', $random_unit); +is($retval, 0, "mask command succeeded"); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +ok(! -e $mask_path, 'mask link does not exist anymore'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “mask” (when disabled) works the same way ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +$retval = dsh('disable', $random_unit); +is($retval, 0, "disable command succeeded"); +ok(! -e $symlink_path, 'symlink no longer exists'); + +$retval = dsh('mask', $random_unit); +is($retval, 0, "mask command succeeded"); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +ok(! -e $mask_path, 'symlink no longer exists'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “mask”/unmask don’t do anything when the user already masked. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +ok(! -l $mask_path, 'mask link does not exist yet'); +symlink('/dev/null', $mask_path); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('mask', $random_unit); +is($retval, 0, "mask command succeeded"); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service still masked'); + +$retval = dsh('unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service still masked'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “mask”/unmask don’t do anything when the user copied the .service. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +unlink($mask_path); + +open($fh, '>', $mask_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=multi-user.target +EOT +close($fh); + +ok(-e $mask_path, 'local service file exists'); +ok(! -l $mask_path, 'local service file is not a symlink'); + +$retval = dsh('mask', $random_unit); +isnt($retval, -1, 'deb-systemd-helper could be executed'); +ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal'); +is($retval >> 8, 0, 'deb-systemd-helper exited with exit code 0'); +ok(-e $mask_path, 'local service file still exists'); +ok(! -l $mask_path, 'local service file is still not a symlink'); + +$retval = dsh('unmask', $random_unit); +isnt($retval, -1, 'deb-systemd-helper could be executed'); +ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal'); +is($retval >> 8, 0, 'deb-systemd-helper exited with exit code 0'); +ok(-e $mask_path, 'local service file still exists'); +ok(! -l $mask_path, 'local service file is still not a symlink'); + +unlink($mask_path); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify Alias= handling. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +$retval = dsh('purge', $random_unit); +is($retval, 0, "purge command succeeded"); + +open($fh, '>', $servicefile_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=multi-user.target +Alias=foo\x2dtest.service +EOT +close($fh); + +isnt_enabled($random_unit); +isnt_enabled('foo\x2dtest.service'); +my $alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dtest.service'; +ok(! -l $alias_path, 'alias link does not exist yet'); +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); +is_enabled($random_unit); +ok(! -l $mask_path, 'mask link does not exist yet'); + +$retval = dsh('mask', $random_unit); +is($retval, 0, "mask command succeeded"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); +ok(! -l $mask_path, 'mask link does not exist any more'); + +$retval = dsh('disable', $random_unit); +isnt_enabled($random_unit); +ok(! -l $alias_path, 'alias link does not exist any more'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify Alias/mask with removed package (as in postrm) ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +$retval = dsh('purge', $random_unit); +is($retval, 0, "purge command succeeded"); +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); + +unlink($servicefile_path); + +$retval = dsh('mask', $random_unit); +is($retval, 0, "mask command succeeded with uninstalled unit"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('purge', $random_unit); +is($retval, 0, "purge command succeeded with uninstalled unit"); +ok(! -l $alias_path, 'alias link does not exist any more'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('unmask', $random_unit); +is($retval, 0, "unmask command succeeded with uninstalled unit"); +ok(! -l $mask_path, 'mask link does not exist any more'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify Alias= to the same unit name ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +open($fh, '>', $servicefile_path); +print $fh <<"EOT"; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=multi-user.target +Alias=$random_unit +EOT +close($fh); + +isnt_enabled($random_unit); +isnt_enabled('foo\x2dtest.service'); +# note that in this case $alias_path and $mask_path are identical +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is_enabled($random_unit); +# systemctl enable does create the alias link even if it's not needed +#ok(! -l $mask_path, 'mask link does not exist yet'); + +unlink($servicefile_path); + +$retval = dsh('mask', $random_unit); +is($retval, 0, "mask command succeeded"); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +ok(! -l $mask_path, 'mask link does not exist any more'); + +$retval = dsh('purge', $random_unit); +isnt_enabled($random_unit); +ok(! -l $mask_path, 'mask link does not exist any more'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify WantedBy and Alias with template unit with DefaultInstance. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +($fh, $servicefile_path) = tempfile('unit\x2dXXXXX', + DIR => "$dpkg_root/lib/systemd/system", + SUFFIX => '@.service', + UNLINK => 1); +print $fh <<'EOT'; +[Unit] +Description=template test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +Alias=foo\x2dtest@.service +Alias=foo\x2dbar@baz.service +WantedBy=multi-user.target +DefaultInstance=instance\x2d +EOT +close($fh); + +$random_unit = basename($servicefile_path); +my $random_instance = $random_unit; +$random_instance =~ s/^(.*\@)(\.\w+)$/$1instance\\x2d$2/; + +isnt_enabled($random_unit); +isnt_enabled('foo\x2dtest@.service'); +isnt_enabled('foo\x2dtest@instance\x2d.service'); +isnt_enabled('foo\x2dbar@baz.service'); +isnt_enabled('foo\x2dbar@instance\x2d.service'); + +my $template_alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dtest@.service'; +my $instance_alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dbar@baz.service'; +my $template_wanted_path = "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit"; +my $instance_wanted_path = "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_instance"; +ok(! -l $template_alias_path, 'template alias link does not exist yet'); +ok(! -l $instance_alias_path, 'instance alias link does not exist yet'); +ok(! -l $template_wanted_path, 'template wanted link does not exist yet'); +ok(! -l $instance_wanted_path, 'instance wanted link does not exist yet'); +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is($dpkg_root . readlink($template_alias_path), $servicefile_path, 'correct template alias link'); +is($dpkg_root . readlink($instance_alias_path), $servicefile_path, 'correct instance alias link'); +ok(! -l $template_wanted_path, 'template wanted link does still not exist'); +is($dpkg_root . readlink($instance_wanted_path), $servicefile_path, 'correct instance wanted link'); +is_enabled($random_unit); + +$retval = dsh('disable', $random_unit); +isnt_enabled($random_unit); +ok(! -l $template_alias_path, 'template alias link does not exist anymore'); +ok(! -l $instance_alias_path, 'instance alias link does not exist anymore'); +ok(! -l $template_wanted_path, 'template wanted link does still not exist'); +ok(! -l $instance_wanted_path, 'instance wanted link does not exist anymore'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify WantedBy and Alias with template unit without DefaultInstance. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +open($fh, '>', $servicefile_path); +print $fh <<'EOT'; +[Unit] +Description=template test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +Alias=foo\x2dtest@.service +Alias=foo\x2dbar@baz.service +RequiredBy=foo\x2ddepender@.service +EOT +close($fh); + +isnt_enabled($random_unit); +isnt_enabled('foo\x2dtest@.service'); +isnt_enabled('foo\x2dbar@baz.service'); + +$template_alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dtest@.service'; +$instance_alias_path = $dpkg_root . '/etc/systemd/system/foo\x2dbar@baz.service'; +$template_wanted_path = $dpkg_root . '/etc/systemd/system/foo\x2ddepender@.service.requires/' . $random_unit; +$instance_wanted_path = $dpkg_root . '/etc/systemd/system/foo\x2ddepender@.service.requires/' . $random_instance; +ok(! -l $template_alias_path, 'template alias link does not exist yet'); +ok(! -l $instance_alias_path, 'instance alias link does not exist yet'); +ok(! -l $template_wanted_path, 'template wanted link does not exist yet'); +ok(! -l $instance_wanted_path, 'instance wanted link does not exist yet'); +$retval = dsh('enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is($dpkg_root . readlink($template_alias_path), $servicefile_path, 'correct template alias link'); +is($dpkg_root . readlink($instance_alias_path), $servicefile_path, 'correct instance alias link'); +is($dpkg_root . readlink($template_wanted_path), $servicefile_path, 'correct template wanted link'); +ok(! -l $instance_wanted_path, 'instance wanted link does still not exist'); +is_enabled($random_unit); + +$retval = dsh('disable', $random_unit); +isnt_enabled($random_unit); +ok(! -l $template_alias_path, 'template alias link does not exist anymore'); +ok(! -l $instance_alias_path, 'instance alias link does not exist anymore'); +ok(! -l $template_wanted_path, 'template wanted link does still not exist'); +ok(! -l $instance_wanted_path, 'instance wanted link does not exist anymore'); + +done_testing; diff --git a/t/002-deb-systemd-helper-update.t b/t/002-deb-systemd-helper-update.t new file mode 100644 index 0000000..7f7d826 --- /dev/null +++ b/t/002-deb-systemd-helper-update.t @@ -0,0 +1,196 @@ +#!perl +# vim:ts=4:sw=4:et + +use strict; +use warnings; +use Test::More; +use Test::Deep qw(:preload cmp_bag); +use File::Temp qw(tempfile tempdir); # in core since perl 5.6.1 +use File::Path qw(make_path); # in core since Perl 5.001 +use File::Basename; # in core since Perl 5 +use FindBin; # in core since Perl 5.00307 + +use lib "$FindBin::Bin/."; +use helpers; + +test_setup(); + +my $dpkg_root = $ENV{DPKG_ROOT} // ''; + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “is-enabled” is not true for a random, non-existing unit file. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my ($fh, $random_unit) = tempfile('unitXXXXX', + SUFFIX => '.service', + TMPDIR => 1, + UNLINK => 1); +close($fh); +$random_unit = basename($random_unit); + +my $statefile = "$dpkg_root/var/lib/systemd/deb-systemd-helper-enabled/$random_unit.dsh-also"; +my $servicefile_path = "$dpkg_root/lib/systemd/system/$random_unit"; +make_path("$dpkg_root/lib/systemd/system"); +open($fh, '>', $servicefile_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=multi-user.target +EOT +close($fh); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “enable” creates the requested symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my $retval = dsh('enable', $random_unit); +my $symlink_path = "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit"; +ok(-l $symlink_path, "$random_unit was enabled"); +is($dpkg_root . readlink($symlink_path), $servicefile_path, + "symlink points to $servicefile_path"); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “is-enabled” now returns true. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +is_enabled($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Modify the unit file and verify that “is-enabled” is no longer true. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +open($fh, '>>', $servicefile_path); +print $fh "Alias=newalias.service\n"; +close($fh); + +isnt_enabled($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “was-enabled” is still true (operates on the state file). ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +$retval = dsh('was-enabled', $random_unit); +isnt($retval, -1, 'deb-systemd-helper could be executed'); +ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal'); +is($retval >> 8, 0, "random unit file was-enabled"); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify the new symlink is not yet in the state file. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +is_deeply( + [ state_file_entries($statefile) ], + [ $symlink_path ], + 'state file does not contain the new link yet'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “enable” creates the new symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my $new_symlink_path = "$dpkg_root/etc/systemd/system/newalias.service"; +ok(! -l $new_symlink_path, 'new symlink does not exist yet'); + +$retval = dsh('enable', $random_unit); +ok(-l $new_symlink_path, 'new symlink was created'); +is($dpkg_root . readlink($new_symlink_path), $servicefile_path, + "symlink points to $servicefile_path"); + +is_enabled($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify the new symlink was recorded in the state file. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +cmp_bag( + [ state_file_entries($statefile) ], + [ $symlink_path, $new_symlink_path ], + 'state file updated'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Modify the unit file and verify that “is-enabled” is no longer true. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +open($fh, '>>', $servicefile_path); +print $fh "Alias=another.service\n"; +close($fh); + +isnt_enabled($random_unit); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “was-enabled” is still true (operates on the state file). ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +$retval = dsh('was-enabled', $random_unit); +isnt($retval, -1, 'deb-systemd-helper could be executed'); +ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal'); +is($retval >> 8, 0, "random unit file was-enabled"); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify the new symlink is not yet in the state file. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +cmp_bag( + [ state_file_entries($statefile) ], + [ $symlink_path, $new_symlink_path ], + 'state file does not contain the new link yet'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “update-state” does not create the symlink, but records it in the ┃ +# ┃ state file. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my $new_symlink_path2 = "$dpkg_root/etc/systemd/system/another.service"; +ok(! -l $new_symlink_path2, 'new symlink does not exist yet'); + +$retval = dsh('update-state', $random_unit); +ok(! -l $new_symlink_path2, 'new symlink still does not exist'); + +isnt_enabled($random_unit); + +cmp_bag( + [ state_file_entries($statefile) ], + [ $symlink_path, $new_symlink_path, $new_symlink_path2 ], + 'state file updated'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Rewrite the original contents and verify “update-state” removes the old ┃ +# ┃ links that are no longer present. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +open($fh, '>', $servicefile_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=multi-user.target +EOT +close($fh); + +unlink($new_symlink_path); + +ok(! -l $new_symlink_path, 'new symlink still does not exist'); +ok(! -l $new_symlink_path2, 'new symlink 2 still does not exist'); + +$retval = dsh('update-state', $random_unit); + +ok(! -l $new_symlink_path, 'new symlink still does not exist'); +ok(! -l $new_symlink_path2, 'new symlink 2 still does not exist'); + +is_enabled($random_unit); + +is_deeply( + [ state_file_entries($statefile) ], + [ $symlink_path ], + 'state file updated'); + + +done_testing; diff --git a/t/003-deb-systemd-helper-complex.t b/t/003-deb-systemd-helper-complex.t new file mode 100644 index 0000000..47d8fb4 --- /dev/null +++ b/t/003-deb-systemd-helper-complex.t @@ -0,0 +1,123 @@ +#!perl +# vim:ts=4:sw=4:et + +use strict; +use warnings; +use Test::More; +use Test::Deep qw(:preload cmp_bag); +use File::Temp qw(tempfile tempdir); # in core since perl 5.6.1 +use File::Path qw(make_path); # in core since Perl 5.001 +use File::Basename; # in core since Perl 5 +use FindBin; # in core since Perl 5.00307 + +use lib "$FindBin::Bin/."; +use helpers; + +test_setup(); + +my $dpkg_root = $ENV{DPKG_ROOT} // ''; + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Create two unit files with random names; they refer to each other (Also=).┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +my ($fh1, $random_unit1) = tempfile('unitXXXXX', + SUFFIX => '.service', + TMPDIR => 1, + UNLINK => 1); +close($fh1); +$random_unit1 = basename($random_unit1); + +my ($fh2, $random_unit2) = tempfile('unitXXXXX', + SUFFIX => '.service', + TMPDIR => 1, + UNLINK => 1); +close($fh2); +$random_unit2 = basename($random_unit2); + +my $servicefile_path1 = "$dpkg_root/lib/systemd/system/$random_unit1"; +my $servicefile_path2 = "$dpkg_root/lib/systemd/system/$random_unit2"; +make_path("$dpkg_root/lib/systemd/system"); +open($fh1, '>', $servicefile_path1); +print $fh1 <<EOT; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=multi-user.target +Also=$random_unit2 +EOT +close($fh1); + +open($fh2, '>', $servicefile_path2); +print $fh2 <<EOT; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=multi-user.target +Alias=alias2.service +Also=$random_unit1 +EOT +close($fh2); + +isnt_enabled($random_unit1); +isnt_enabled($random_unit2); +isnt_debian_installed($random_unit1); +isnt_debian_installed($random_unit2); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “enable” creates all symlinks. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +unless ($ENV{'TEST_ON_REAL_SYSTEM'}) { + # This might already exist if we don't start from a fresh directory + ok(! -d "$dpkg_root/etc/systemd/system/multi-user.target.wants", + 'multi-user.target.wants does not exist yet'); +} + +my $retval = dsh('enable', $random_unit1); +my %links = map { (basename($_), $dpkg_root . readlink($_)) } + ("$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit1", + "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit2"); +is_deeply( + \%links, + { + $random_unit1 => $servicefile_path1, + $random_unit2 => $servicefile_path2, + }, + 'All expected links present'); + +my $alias_path = "$dpkg_root/etc/systemd/system/alias2.service"; +ok(-l $alias_path, 'alias created'); +is($dpkg_root . readlink($alias_path), $servicefile_path2, + 'alias points to the correct service file'); + +cmp_bag( + [ state_file_entries("$dpkg_root/var/lib/systemd/deb-systemd-helper-enabled/$random_unit1.dsh-also") ], + [ "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit1", + "$dpkg_root/etc/systemd/system/multi-user.target.wants/$random_unit2", + "$dpkg_root/etc/systemd/system/alias2.service" ], + 'state file updated'); + +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ Verify “is-enabled” now returns true. ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +is_enabled($random_unit1); +is_enabled($random_unit2); +is_debian_installed($random_unit1); + +# $random_unit2 was only enabled _because of_ $random_unit1’s Also= statement +# and thus does not have its own state file. +isnt_debian_installed($random_unit2); + +# TODO: cleanup tests? + +done_testing; diff --git a/t/004-deb-systemd-helper-user.t b/t/004-deb-systemd-helper-user.t new file mode 100644 index 0000000..bd914cb --- /dev/null +++ b/t/004-deb-systemd-helper-user.t @@ -0,0 +1,413 @@ +#!perl +# vim:ts=4:sw=4:et + +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempfile tempdir); # in core since perl 5.6.1 +use File::Path qw(make_path); # in core since Perl 5.001 +use File::Basename; # in core since Perl 5 +use FindBin; # in core since Perl 5.00307 + +use lib "$FindBin::Bin/."; +use helpers; + +test_setup(); + +my $dpkg_root = $ENV{DPKG_ROOT} // ''; + +# +# "is-enabled" is not true for a random, non-existing unit file +# + +my ($fh, $random_unit) = tempfile('unit\x2dXXXXX', + SUFFIX => '.service', + TMPDIR => 1, + UNLINK => 1); +close($fh); +$random_unit = basename($random_unit); + +isnt_enabled($random_unit); +isnt_enabled($random_unit, user => 1); +isnt_debian_installed($random_unit); +isnt_debian_installed($random_unit, user => 1); + +# +# "is-enabled" is not true for a random, existing user unit file +# + +my $servicefile_path = "$dpkg_root/usr/lib/systemd/user/$random_unit"; +make_path("$dpkg_root/usr/lib/systemd/user"); +open($fh, '>', $servicefile_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=default.target +EOT +close($fh); + +isnt_enabled($random_unit); +isnt_enabled($random_unit, user => 1); +isnt_debian_installed($random_unit); +isnt_debian_installed($random_unit, user => 1); + +# +# "enable" creates the requested symlinks +# + +unless ($ENV{'TEST_ON_REAL_SYSTEM'}) { + # This might exist if we don't start from a fresh directory + ok(! -d "$dpkg_root/etc/systemd/user/default.target.wants", + 'default.target.wants does not exist yet'); +} + +my $retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); +my $symlink_path = "$dpkg_root/etc/systemd/user/default.target.wants/$random_unit"; +ok(-l $symlink_path, "$random_unit was enabled"); +is($dpkg_root . readlink($symlink_path), $servicefile_path, + "symlink points to $servicefile_path"); + +# +# "is-enabled" now returns true for the user instance +# + +isnt_enabled($random_unit); +isnt_debian_installed($random_unit); +is_enabled($random_unit, user => 1); +is_debian_installed($random_unit, user => 1); + +# +# deleting the symlinks and running "enable" again does not re-create them +# + +unlink($symlink_path); +ok(! -l $symlink_path, 'symlink deleted'); +isnt_enabled($random_unit); +isnt_enabled($random_unit, user => 1); +isnt_debian_installed($random_unit); +is_debian_installed($random_unit, user => 1); + +$retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); + +isnt_enabled($random_unit, user => 1); + +# +# "disable" deletes the statefile when purging +# + +my $statefile = "$dpkg_root/var/lib/systemd/deb-systemd-user-helper-enabled/$random_unit.dsh-also"; + +ok(-f $statefile, 'state file exists'); + +$ENV{'_DEB_SYSTEMD_HELPER_PURGE'} = '1'; +$retval = dsh('--user', 'disable', $random_unit); +delete $ENV{'_DEB_SYSTEMD_HELPER_PURGE'}; +is($retval, 0, "disable command succeeded"); +ok(! -f $statefile, 'state file does not exist anymore after purging'); +isnt_debian_installed($random_unit); +isnt_debian_installed($random_unit, user => 1); + +# +# "enable" re-creates the symlinks after purging +# + +ok(! -l $symlink_path, 'symlink does not exist yet'); +isnt_enabled($random_unit); +isnt_enabled($random_unit, user => 1); + +$retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); + +isnt_enabled($random_unit); +isnt_debian_installed($random_unit); +is_enabled($random_unit, user => 1); +is_debian_installed($random_unit, user => 1); + +# +# "disable" removes the symlinks +# + +$ENV{'_DEB_SYSTEMD_HELPER_PURGE'} = '1'; +$retval = dsh('--user', 'disable', $random_unit); +delete $ENV{'_DEB_SYSTEMD_HELPER_PURGE'}; +is($retval, 0, "disable command succeeded"); + +isnt_enabled($random_unit); +isnt_enabled($random_unit, user => 1); + +# +# "enable" re-creates the symlinks +# + +ok(! -l $symlink_path, 'symlink does not exist yet'); +isnt_enabled($random_unit); +isnt_enabled($random_unit, user => 1); + +$retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); + +isnt_enabled($random_unit); +isnt_debian_installed($random_unit); +is_enabled($random_unit, user => 1); +is_debian_installed($random_unit, user => 1); + +# +# "purge" works +# + +$retval = dsh('--user', 'purge', $random_unit); +is($retval, 0, "purge command succeeded"); + +isnt_enabled($random_unit, user => 1); +isnt_debian_installed($random_unit, user => 1); + +# +# "enable" re-creates the symlinks after purging +# + +ok(! -l $symlink_path, 'symlink does not exist yet'); +isnt_enabled($random_unit, user => 1); + +$retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); + +is_enabled($random_unit, user => 1); +is_debian_installed($random_unit, user => 1); + +# +# "mask" (when enabled) results in the symlink pointing to /dev/null +# + +my $mask_path = "$dpkg_root/etc/systemd/user/$random_unit"; +ok(! -l $mask_path, 'mask link does not exist yet'); + +$retval = dsh('--user', 'mask', $random_unit); +is($retval, 0, "mask command succeeded"); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('--user', 'unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +ok(! -e $mask_path, 'mask link does not exist anymore'); + +# +# "mask" (when disabled) works the same way +# + +$retval = dsh('--user', 'disable', $random_unit); +is($retval, 0, "disable command succeeded"); +ok(! -e $symlink_path, 'symlink no longer exists'); + +$retval = dsh('--user', 'mask', $random_unit); +is($retval, 0, "mask command succeeded"); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('--user', 'unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +ok(! -e $mask_path, 'symlink no longer exists'); + +# +# "mask" / "unmask" don't do anything when the unit is already masked +# + +ok(! -l $mask_path, 'mask link does not exist yet'); +symlink('/dev/null', $mask_path); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('--user', 'mask', $random_unit); +is($retval, 0, "mask command succeeded"); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service still masked'); + +$retval = dsh('--user', 'unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +ok(-l $mask_path, 'mask link exists'); +is(readlink($mask_path), '/dev/null', 'service still masked'); + +# +# "mask" / "unmask" don't do anything when the user copied the .service. +# + +unlink($mask_path); + +open($fh, '>', $mask_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=default.target +EOT +close($fh); + +ok(-e $mask_path, 'local service file exists'); +ok(! -l $mask_path, 'local service file is not a symlink'); + +$retval = dsh('--user', 'mask', $random_unit); +isnt($retval, -1, 'deb-systemd-helper could be executed'); +ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal'); +is($retval >> 8, 0, 'deb-systemd-helper exited with exit code 0'); +ok(-e $mask_path, 'local service file still exists'); +ok(! -l $mask_path, 'local service file is still not a symlink'); + +$retval = dsh('--user', 'unmask', $random_unit); +isnt($retval, -1, 'deb-systemd-helper could be executed'); +ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal'); +is($retval >> 8, 0, 'deb-systemd-helper exited with exit code 0'); +ok(-e $mask_path, 'local service file still exists'); +ok(! -l $mask_path, 'local service file is still not a symlink'); + +unlink($mask_path); + +# +# "Alias=" handling +# + +$retval = dsh('--user', 'purge', $random_unit); +is($retval, 0, "purge command succeeded"); + +open($fh, '>', $servicefile_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=default.target +Alias=foo\x2dtest.service +EOT +close($fh); + +isnt_enabled($random_unit, user => 1); +isnt_enabled('foo\x2dtest.service', user => 1); +my $alias_path = $dpkg_root . '/etc/systemd/user/foo\x2dtest.service'; +ok(! -l $alias_path, 'alias link does not exist yet'); +$retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); +is_enabled($random_unit, user => 1); +ok(! -l $mask_path, 'mask link does not exist yet'); + +$retval = dsh('--user', 'mask', $random_unit); +is($retval, 0, "mask command succeeded"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('--user', 'unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); +ok(! -l $mask_path, 'mask link does not exist any more'); + +$retval = dsh('--user', 'disable', $random_unit); +isnt_enabled($random_unit, user => 1); +ok(! -l $alias_path, 'alias link does not exist any more'); + +# +# "Alias=" / "mask" with removed package (as in postrm) +# + +$retval = dsh('--user', 'purge', $random_unit); +is($retval, 0, "purge command succeeded"); +$retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); + +unlink($servicefile_path); + +$retval = dsh('--user', 'mask', $random_unit); +is($retval, 0, "mask command succeeded with uninstalled unit"); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('--user', 'purge', $random_unit); +is($retval, 0, "purge command succeeded with uninstalled unit"); +ok(! -l $alias_path, 'alias link does not exist any more'); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('--user', 'unmask', $random_unit); +is($retval, 0, "unmask command succeeded with uninstalled unit"); +ok(! -l $mask_path, 'mask link does not exist any more'); + +# +# "Alias=" to the same unit name +# + +open($fh, '>', $servicefile_path); +print $fh <<"EOT"; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +WantedBy=default.target +Alias=$random_unit +EOT +close($fh); + +isnt_enabled($random_unit, user => 1); +isnt_enabled('foo\x2dtest.service', user => 1); +# note that in this case $alias_path and $mask_path are identical +$retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is_enabled($random_unit, user => 1); +# systemctl enable does create the alias link even if it's not needed +#ok(! -l $mask_path, 'mask link does not exist yet'); + +unlink($servicefile_path); + +$retval = dsh('--user', 'mask', $random_unit); +is($retval, 0, "mask command succeeded"); +is(readlink($mask_path), '/dev/null', 'service masked'); + +$retval = dsh('--user', 'unmask', $random_unit); +is($retval, 0, "unmask command succeeded"); +ok(! -l $mask_path, 'mask link does not exist any more'); + +$retval = dsh('--user', 'purge', $random_unit); +isnt_enabled($random_unit, user => 1); +ok(! -l $mask_path, 'mask link does not exist any more'); + +# +# "Alias=" without "WantedBy=" +# + +open($fh, '>', $servicefile_path); +print $fh <<'EOT'; +[Unit] +Description=test unit + +[Service] +ExecStart=/bin/sleep 1 + +[Install] +Alias=baz\x2dtest.service +EOT +close($fh); + +isnt_enabled($random_unit, user => 1); +isnt_enabled('baz\x2dtest.service', user => 1); +$alias_path = $dpkg_root . '/etc/systemd/user/baz\x2dtest.service'; +ok(! -l $alias_path, 'alias link does not exist yet'); +$retval = dsh('--user', 'enable', $random_unit); +is($retval, 0, "enable command succeeded"); +is_enabled($random_unit, user => 1); +ok(-l $alias_path, 'alias link does exist'); +is($dpkg_root . readlink($alias_path), $servicefile_path, 'correct alias link'); + +done_testing; diff --git a/t/README b/t/README new file mode 100644 index 0000000..eda327d --- /dev/null +++ b/t/README @@ -0,0 +1,15 @@ +These test cases need the Linux::Clone module, which is not yet in Debian. +See http://michael.stapelberg.de/cpan/#Linux::Clone on how to install it. + +Note that you need to run the test cases as root because they use Linux mount +namespaces and bind mounts (requires Linux ≥ 2.4.19). + +Note that you should mark / as a private subtree before running these tests, or +they will fail. Use mount --make-rprivate /. Unfortunately, the version of +util-linux in Debian at the time of writing (2.20.1) is broken and its +--make-rprivate does not actually work. See #731574. + +The intention is that the testcases are _not_ run automatically during package +building because they might be too fragile and additional dependencies make it +harder to port this package to Ubuntu or Debian backports. It is enough if the +test cases are run on every code change. diff --git a/t/helpers.pm b/t/helpers.pm new file mode 100644 index 0000000..72d4f68 --- /dev/null +++ b/t/helpers.pm @@ -0,0 +1,160 @@ +use strict; +use warnings; +use English; +use File::Temp qw(tempdir); # in core since perl 5.6.1 +use File::Copy qw(cp); +use File::Path qw(make_path); + +sub check_fakechroot_running() { + my $content = `FAKECHROOT_DETECT=1 sh -c "echo This should not be printed"`; + my $result = 0; + if ($content =~ /^fakechroot [0-9.]+\n$/) { + $result = 1; + } + return $result; +} + +sub test_setup() { + if (length $ENV{TEST_DPKG_ROOT}) { + print STDERR "test_setup() with DPKG_ROOT\n"; + $ENV{DPKG_ROOT} = tempdir( CLEANUP => 1 ); + return; + } + + if ( !check_fakechroot_running ) { + print STDERR "you have to run this script inside fakechroot and fakeroot:\n"; + print STDERR (" fakechroot fakeroot perl $PROGRAM_NAME" . (join " ", @ARGV) . "\n"); + exit 1; + } + + # Set up a chroot that contains everything necessary to run + # deb-systemd-helper under fakechroot. + print STDERR "test_setup() with fakechroot\n"; + + my $tmpdir = tempdir( CLEANUP => 1 ); + mkdir "$tmpdir/dev"; + 0 == system 'mknod', "$tmpdir/dev/null", 'c', '1', '3' or die "cannot mknod: $?"; + mkdir "$tmpdir/tmp"; + make_path("$tmpdir/usr/bin"); + make_path("$tmpdir/usr/lib/systemd/user"); + make_path("$tmpdir/lib/systemd/system/"); + make_path("$tmpdir/var/lib/systemd"); + make_path("$tmpdir/etc/systemd"); + if ( length $ENV{TEST_INSTALLED} ) { + # if we test the installed deb-systemd-helper we copy it from the + # system's installation + cp "/usr/bin/deb-systemd-helper", "$tmpdir/usr/bin/deb-systemd-helper" + or die "cannot copy: $!"; + } + else { + cp "$FindBin::Bin/../script/deb-systemd-helper", + "$tmpdir/usr/bin/deb-systemd-helper" + or die "cannot copy: $!"; + } + + # make sure that dpkg diversion messages are not translated + local $ENV{LC_ALL} = 'C.UTF-8'; + # the chroot only needs to contain a working perl-base + open my $fh, '-|', 'dpkg-query', '--listfiles', 'perl-base'; + + while ( my $path = <$fh> ) { + chomp $path; + # filter out diversion messages in the same way that dpkg-repack does + # https://git.dpkg.org/cgit/dpkg/dpkg-repack.git/tree/dpkg-repack#n238 + if ($path =~ /^package diverts others to: /) { + next; + } + if ($path =~ /^diverted by [^ ]+ to: /) { + next; + } + if ($path =~ /^locally diverted to: /) { + next; + } + if ($path !~ /^\//) { + die "path must start with a slash"; + } + if ( -e "$tmpdir$path" ) { + # ignore paths that were already created + next; + } elsif ( !-r $path ) { + # if the host's path is not readable, assume it's a directory + mkdir "$tmpdir$path" or die "cannot mkdir $path: $!"; + } elsif ( -l $path ) { + symlink readlink($path), "$tmpdir$path"; + } elsif ( -d $path ) { + mkdir "$tmpdir$path" or die "cannot mkdir $path: $!"; + } elsif ( -f $path ) { + cp $path, "$tmpdir$path" or die "cannot cp $path: $!"; + } else { + die "cannot handle $path"; + } + } + close $fh; + + $ENV{'SYSTEMCTL_INSTALL_CLIENT_SIDE'} = '1'; + + # we run the chroot call in a child process because we need the parent + # process remaining un-chrooted or otherwise it cannot clean-up the + # temporary directory on exit + my $pid = fork() // die "cannot fork: $!"; + if ( $pid == 0 ) { + chroot $tmpdir or die "cannot chroot: $!"; + chdir "/" or die "cannot chdir to /: $!"; + return; + } + waitpid($pid, 0); + + exit $?; +} + +# reads in a whole file +sub slurp { + open my $fh, '<', shift; + local $/; + <$fh>; +} + +sub state_file_entries { + my ($path) = @_; + my $bytes = slurp($path); + my $dpkg_root = $ENV{DPKG_ROOT} // ''; + return map { "$dpkg_root$_" } split("\n", $bytes); +} + +my $dsh = ''; +if ( length $ENV{TEST_INSTALLED} ) { + # if we are to test the installed version of deb-systemd-helper then even + # in DPKG_ROOT mode, we want to run /usr/bin/deb-systemd-helper + $dsh = "/usr/bin/deb-systemd-helper"; +} else { + if ( length $ENV{TEST_DPKG_ROOT} ) { + # when testing deb-systemd-helper from source, then in DPKG_ROOT mode, + # we take the script from the source directory + $dsh = "$FindBin::Bin/../script/deb-systemd-helper"; + } else { + $dsh = "/usr/bin/deb-systemd-helper"; + } +} +$ENV{'DPKG_MAINTSCRIPT_PACKAGE'} = 'deb-systemd-helper-test'; + +sub dsh { + return system($dsh, @_); +} + +sub _unit_check { + my ($cmd, $cb, $verb, $unit, %opts) = @_; + + my $retval = dsh($opts{'user'} ? '--user' : '--system', $cmd, $unit); + + isnt($retval, -1, 'deb-systemd-helper could be executed'); + ok(!($retval & 127), 'deb-systemd-helper did not exit due to a signal'); + $cb->($retval >> 8, 0, "random unit file '$unit' $verb $cmd"); +} + +sub is_enabled { _unit_check('is-enabled', \&is, 'is', @_) } +sub isnt_enabled { _unit_check('is-enabled', \&isnt, 'isnt', @_) } + +sub is_debian_installed { _unit_check('debian-installed', \&is, 'is', @_) } +sub isnt_debian_installed { _unit_check('debian-installed', \&isnt, 'isnt', @_) } + +1; |