summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/README.invoke-rc.d135
-rw-r--r--doc/README.policy-rc.d102
-rw-r--r--man8/invoke-rc.d.rst223
-rw-r--r--man8/service.rst88
-rw-r--r--man8/update-rc.d.rst249
-rwxr-xr-xscript/deb-systemd-helper691
-rwxr-xr-xscript/deb-systemd-invoke187
-rwxr-xr-xscript/invoke-rc.d574
-rwxr-xr-xscript/service217
-rwxr-xr-xscript/update-rc.d543
-rw-r--r--t/001-deb-systemd-helper.t473
-rw-r--r--t/002-deb-systemd-helper-update.t196
-rw-r--r--t/003-deb-systemd-helper-complex.t123
-rw-r--r--t/004-deb-systemd-helper-user.t413
-rw-r--r--t/README15
-rw-r--r--t/helpers.pm160
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;