1
#!/bin/bash
sleep 2
nohup java -jar /home/ubuntu/radius/radius-test.jar >> /home/ubuntu/radius/radius.log &
nohup java -jar /home/ubuntu/radius/radius-test-2.jar >> /home/ubuntu/radius/radius-2.log &

I have a script file like the one above and it should run automatically at boot time.

Among the many methods I found out, I decided to use systemd and succeeded in getting the desired behavior.

However, I would like to know if there is a correct way to specify the Type option or if there is no problem.

[Unit]
Description=My Shell Script

[Service]
Type=simple or oneshot
RemainAfterExit=yes
ExecStart=/home/ubuntu/radius/radius-start.sh

[Install]
WantedBy=multi-user.target


---

[Unit]
Description=My Shell Script

[Service]
Type=forking
ExecStart=/home/ubuntu/radius/radius-start.sh

[Install]
WantedBy=multi-user.target

In both methods, the service runs when booting up, and my jar file runs as well. And the service is still active and the jar process is maintained as well. Then, when the service is terminated, all jar processes are also terminated.

I looked it up and found that forking is suitable for a background method, the child process runs, and then the parent process dies. (I don't understand this part either.) It also says that a pid file must be specified or that the forking method is not used.

Simple or oneshot is said to be used in a way that requires operation in the foreground. But my jar process needs to run in the background.

What is the difference between the two methods? Which option is the correct way to use it?

1 Answer 1

1

Let's look at the two versions of your unit file, and then I'll show you what I would suggest:

simple or oneshot service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/home/ubuntu/radius/radius-start.sh

Type=oneshot is more appropriate to use with RemainAfterExit=yes than Type=simple.

With simple you normally run ExecStart=. Then when that process ends, everything is cleaned up and the service is "stopped". However, radius-start.sh launches some processes and then stops immediately. That means systemd will look for any orphaned processes and kill them as part of the clean-up.

With oneshot, that is really meant to run ExecStart= and then die. RemainAfterExit= can use useful to help you trigger ExecStop= commands or make use of ConsistsOf= relationships to stop other units when this unit is stopped. But it isn't giving you an up-to-date status of the child processes you spawned.

The big issue with this technique is that it will either kill your (orphaned) child processes immediately, or it will not understand that the unit is stopped/failed when the child processes exit.


forking service

[Service]
Type=forking
ExecStart=/home/ubuntu/radius/radius-start.sh

This is the correct version of your service. forking will run ExecStart=, which is expected to exit very quickly. systemd then expects some spawned child processes, and will track the state of those. When the main process exits, systemd will consider the service to be stopped.

If you stick with this method, then it is recommended to set PIDFile= so that systemd understands which of your two processes is the main one.

[Service]
...
PIDFile=/run/myservice.pid
...
#!/bin/bash
sleep 2
nohup java -jar /home/ubuntu/radius/radius-test.jar >> /home/ubuntu/radius/radius.log &
echo $! > /run/myservice.pid
nohup java -jar /home/ubuntu/radius/radius-test-2.jar >> /home/ubuntu/radius/radius-2.log &

However, systemd's documentation explicitly states:

Note that PID files should be avoided in modern projects. Use Type=notify or Type=simple where possible, which does not require use of PID files to determine the main process of a service and avoids needless forking.

Calling bash here to run your process is unnecessary as everything you want to do is already supplied by systemd natively.


Verbatim suggestion with Type=simple

If I were to implement your service verbatim, I would implement it with two services:

# radius-test.service
[Service]
Type=simple
ExecStartPre=/bin/sleep 2
ExecStart=/usr/bin/java -jar /home/ubuntu/radius/radius-test.jar
StandardOutput=append:/home/ubuntu/radius/radius.log
# radius-test2.service
[Unit]
After=radius-test.service

[Service]
Type=simple
ExecStart=/usr/bin/java -jar /home/ubuntu/radius/radius-test-2.jar
StandardOutput=append:/home/ubuntu/radius/radius-2.log

Now if one of the services fails, we can detect which one failed and recover it. If these services need some sort of relationship, those can be defined. Ordering, for example, can be achieved with After=. If you want one to stop cleanly when the other fails, you could add BindsTo=.


Improving the solution

  • ExecStartPre=/bin/sleep 2: This is usually a hack. A delay introduces unnecessary delay on good days, or not enough delay on bad days. It's better to understand why you needed to sleep. For example, if you need the network to be available before running your service, use After=network.target instead.
  • Type=simple: This is actually the default. So the line can be removed.
  • If you always want to run radius-test2 when starting radius-test, then use a Wants= relationship (or specify WantedBy=radius-test in radius-test2's [Install] section)
  • If you want to stop radius-test2 when radius-test is stopped, then use a PartOf= relationship.
  • StandardOutput=append:...: While there isn't a problem with logging things to your own file, you may find it convenient to use the built-in journal. By simply removing this line, you can get your logs via journalctl -u myservice.service. journalctl provides lots of extra functionality like log rotating, size limits, and filtering. I frequently do stuff like this:

journalctl -u myservice --since "5 hours ago" --until "4 hours ago"

An improved version would be this:

# /etc/systemd/system/radius-test.service
[Unit]
Description=Radius Test 1
After=network.target
Before=radius-test-2
Wants=radius-test-2

[Service]
ExecStart=/usr/bin/java -jar /home/ubuntu/radius/radius-test.jar

[Install]
WantedBy=multi-user-target

# /etc/systemd/system/radius-test2.service
[Unit]
Description=Radius Test 2
PartOf=radius-test

[Service]
ExecStart=/usr/bin/java -jar /home/ubuntu/radius/radius-test-2.jar

The final simple solution

However, now that we have removed StandardOutput= and used the journal, it's also possible to combine these into one unit. This is a simpler solution using ExecStartPost=:

# /etc/systemd/system/radius-test.service
[Unit]
Description=Radius Test
After=network.target

[Service]
ExecStart=/usr/bin/java -jar /home/ubuntu/radius/radius-test.jar
ExecStartPost=/usr/bin/java -jar /home/ubuntu/radius/radius-test-2.jar

[Install]
WantedBy=multi-user-target
4
  • Thank you for answer. I followed your example and it works correctly. However, the worrying part is that the content of the script I presented as an example is very short. If this script is long, is it still correct to input everything into the service? What should I do if I have to use a script (sh)?
    – 남혁준
    Feb 22, 2023 at 6:31
  • Most scripts follow the same type of pattern: set some environment variables, setup logging, fork a process, save the PID, and exit. Instead of writing your own bash script to do it, systemd implements those patterns in its service, providing more globally configurable and consistent behaviour. Even things like conditionals are supported. If you ask another question with a larger script, we can show you how to systemd-ify it.
    – Stewart
    Feb 22, 2023 at 8:34
  • If you HAVE to use a script (this often means you are running some older software daemon-ized in the sysV days), then forking is the correct solution.
    – Stewart
    Feb 22, 2023 at 8:34
  • I learned something new about systemd. Thank you.
    – 남혁준
    Feb 22, 2023 at 9:01

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .