diff --git a/package.xml b/package.xml index 0605a1f..255c866 100644 --- a/package.xml +++ b/package.xml @@ -18,6 +18,7 @@ daemontools net-tools roslaunch + supervisor util-linux xacro diff --git a/src/robot_upstart/install_script.py b/src/robot_upstart/install_script.py index 29fbd77..a5cb547 100644 --- a/src/robot_upstart/install_script.py +++ b/src/robot_upstart/install_script.py @@ -62,7 +62,7 @@ def get_argument_parser(): help="Specify an a value for ROS_LOG_DIR in the job launch context.") p.add_argument("--augment", action='store_true', help="Bypass creating the job, and only copy user files. Assumes the job was previously created.") - p.add_argument("--provider", type=str, metavar="[upstart|systemd]", + p.add_argument("--provider", type=str, metavar="[upstart|systemd|supervisor]", help="Specify provider if the autodetect fails to identify the correct provider") p.add_argument("--symlink", action='store_true', help="Create symbolic link to job launch files instead of copying them.") @@ -71,6 +71,9 @@ def get_argument_parser(): p.add_argument("--systemd-after", type=str, metavar="After=", help="Set the string of the After= section" "of the generated Systemd service file") + p.add_argument("--supervisor-priority", type=int, metavar="Priority=", + help="Set the value of the priority= section" + "of the generated Supervisor conf file") return p @@ -89,7 +92,9 @@ def main(): name=job_name, interface=args.interface, user=args.user, workspace_setup=args.setup, rosdistro=args.rosdistro, master_uri=args.master, log_path=args.logdir, - systemd_after=args.systemd_after) + sigterm_stop=(args.provider=='supervisor'), + systemd_after=args.systemd_after, + supervisor_priority=args.supervisor_priority) for this_pkgpath in args.pkgpath: pkg, pkgpath = this_pkgpath.split('/', 1) @@ -121,6 +126,8 @@ def main(): provider = providers.Upstart if args.provider == 'systemd': provider = providers.Systemd + if args.provider == 'supervisor': + provider = providers.Supervisor if args.symlink: j.symlink = True diff --git a/src/robot_upstart/job.py b/src/robot_upstart/job.py index 895525c..561680d 100644 --- a/src/robot_upstart/job.py +++ b/src/robot_upstart/job.py @@ -40,8 +40,8 @@ class Job(object): """ Represents a ROS configuration to launch on machine startup. """ def __init__(self, name="ros", interface=None, user=None, workspace_setup=None, - rosdistro=None, master_uri=None, log_path=None, - systemd_after=None): + rosdistro=None, master_uri=None, log_path=None, sigterm_stop=None, + systemd_after=None, supervisor_priority=None): """Construct a new Job definition. :param name: Name of job to create. Defaults to "ros", but you might @@ -105,6 +105,13 @@ def __init__(self, name="ros", interface=None, user=None, workspace_setup=None, # of the generated Systemd service file self.systemd_after = systemd_after or "network.target" + # Set the value of the "priority=" section + # of the generated Supservisor conf file + self.supervisor_priority = supervisor_priority or 200 + + # call @(name)-stop script when received TERM signal + self.sigterm_stop = sigterm_stop + # Set of files to be installed for the job. This is only launchers # and other user-specified configs--- nothing related to the system # startup job itself. List of strs. diff --git a/src/robot_upstart/providers.py b/src/robot_upstart/providers.py index a91b6bd..a27cf84 100644 --- a/src/robot_upstart/providers.py +++ b/src/robot_upstart/providers.py @@ -40,6 +40,8 @@ def detect_provider(): print(os.path.realpath(cmd)) if b'systemd' in os.path.realpath(cmd): return Systemd + if b'supervisor' in os.path.realpath(cmd): + return Supervisor return Upstart @@ -228,3 +230,78 @@ def _fill_template(self, template): self.interpreter.file(f) return self.interpreter.output.getvalue() self.set_job_path() + + +class Supervisor(Generic): + """ The Supervisor implementation places the user-specified files in ``/etc/ros/DISTRO/NAME.d``, + and creates an systemd job configuration in ``/lib/systemd/system/NAME.d``. Two additional + helper scripts are created for starting and stopping the job, places in + ``/usr/sbin``. + To detect which system you're using run: ps -p1 | grep systemd && echo systemd || echo upstart + """ + + def generate_install(self): + # Default is /etc/ros/DISTRO/JOBNAME.d + self._set_job_path() + + # User-specified launch files. + self._add_job_files() + + # This is optional to support the old --augment flag where a "job" only adds + # launch files to an existing configuration. + if self.job.generate_system_files: + # Share a single instance of the EmPy interpreter. + self.interpreter = em.Interpreter(globals=self.job.__dict__.copy()) + + self.installation_files[os.path.join(self.root, "etc/supervisor/conf.d", self.job.name + ".conf")] = { + "content": self._fill_template("templates/supervisor_job.conf.em"), "mode": 0o644} + self.installation_files[os.path.join(self.root, "usr/sbin", self.job.name + "-start")] = { + "content": self._fill_template("templates/job-start.em"), "mode": 0o755} + self.installation_files[os.path.join(self.root, "usr/sbin", self.job.name + "-stop")] = { + "content": self._fill_template("templates/job-stop.em"), "mode": 0o755} + self.interpreter.shutdown() + + # Add an annotation file listing what has been installed. This is a union of what's being + # installed now with what has been installed previously, so that an uninstall should remove + # all of it. A more sophisticated future implementation could track contents or hashes and + # thereby warn users when a new installation is stomping a change they have made. + self._load_installed_files_set() + self.installed_files_set.update(list(self.installation_files.keys())) + + # Remove the job directory. This will fail if it is not empty, and notify the user. + self.installed_files_set.add(self.job.job_path) + + # Remove the annotation file itself. + self.installed_files_set.add(self.installed_files_set_location) + + self.installation_files[self.installed_files_set_location] = { + "content": "\n".join(self.installed_files_set)} + + return self.installation_files + + def post_install(self): + print("** To complete installation please run the following command:") + print(" sudo supervisorctl reload" + + " && sudo supervisorctl status" + + " ; browse https://localhost:9001") + + def generate_uninstall(self): + self._set_job_path() + self._load_installed_files_set() + + for filename in self.installed_files_set: + self.installation_files[filename] = {"remove": True} + + return self.installation_files + + def _set_job_path(self): + self.job.job_path = os.path.join( + self.root, "etc/ros", self.job.rosdistro, self.job.name + ".d") + + def _fill_template(self, template): + self.interpreter.output = io.StringIO() + self.interpreter.reset() + with open(find_in_workspaces(project="robot_upstart", path=template)[0]) as f: + self.interpreter.file(f) + return self.interpreter.output.getvalue() + self.set_job_path() diff --git a/templates/job-start.em b/templates/job-start.em index d69fd03..e31a96b 100644 --- a/templates/job-start.em +++ b/templates/job-start.em @@ -107,6 +107,13 @@ fi setpriv --reuid @(user) --regid @(user) --init-groups roslaunch $LAUNCH_FILENAME @(roslaunch_wait?'--wait ')& PID=$! +@[if sigterm_stop]@ +_term() { + /usr/sbin/@(name)-stop +} +trap _term SIGTERM +@[end if] + log info "@(name): Started roslaunch as background process, PID $PID, ROS_LOG_DIR=$ROS_LOG_DIR" echo "$PID" > $log_path/@(name).pid diff --git a/templates/supervisor_job.conf.em b/templates/supervisor_job.conf.em new file mode 100644 index 0000000..ba6c550 --- /dev/null +++ b/templates/supervisor_job.conf.em @@ -0,0 +1,34 @@ +@# +@# Author: Kei Okada +@# Copyright (c) 2022 +@# +@# 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 the copyright holder nor the +@# names of its contributors may be used to endorse or promote products +@# derived from this software without specific prior written permission. +@# +@# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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. +@# +# THIS IS A GENERATED FILE, NOT RECOMMENDED TO EDIT. + +[program:@(name)] +command=/usr/sbin/@(name)-start +stopsignal=TERM +autostart=true +autorestart=false +priority=@(supervisor_priority)