4

We are running a Golang application that internally runs an autoupdate module to recieve over the air updates. At the time of a new update, the autoupdate module downloads the new version from s3 and invokes an autoupdate bash script present in the same working directory as the executable. The steps performed by the autoupdate bash script are explained in terms of process management handled with and without systemd.

Execution without Systemd

  1. Kills the currently running golang app via PID
  2. Runs the newly downloaded golang binary.
  3. The new version exposes an health check API which is invoked by script to makesure this version is healthy.
  4. If not healthy, rollback is performed. The new version is stopped and older version is started.

In golang, the script execution code is written such that when autoupdate script is invoked, it is disowned from the lifecycle of the golang application. i.e bash script (child process) continues execution even after the parent(golang app) is killed. It works fine when running from the GoLand IDE.

Issue : Execution via Systemd

We needed a cleaner system to handle the start ,stop, restart-on failure functionalities. So we decided to run the app as a systemd service.The steps are the same as above but performed using systemctl

  1. Kills the currently running golang app : "systemctl stop goapp.service"
  2. Updates the systemd service with the new executable path : "systemctl daemon-reload"
  3. Restarts the systemd service : "systemctl restart goapp.service"
  4. If not healthy, rollback is performed. The new version is stopped, systemd service file is updated with older version and older version is started.

On executing the application via systemd, the script exits as soon as the systemctl service is stopped using command "sudo systemctl stop goapp.service" during step 1 mentioned above. This means that the lifecycle of the script executed by the golang app is clearly coupled to the lifecycle of the systemd service. How can I detach it from the of scope of systemd service?

Systemd service file

[Unit]
Description=Goapp
After=docker.service
Requires=docker.service
Requires=docker-mysql.service
Requires=docker-redis.service
StartLimitInterval=200
StartLimitBurst=5

[Service]
User=root
Environment=AWS_SHARED_CREDENTIALS_FILE=/home/username/.aws/credentials
Restart=on-failure
RestartSec=30
WorkingDirectory=/home/username/go/src/goapp-v2.0.0
ExecStart=/home/username/go/src/goapp-v2.0.0/goexecutable --autoupdate=true

[Install]
WantedBy=multi-user.target

The Golang code section that invokes the autoupdate bashscript

func scriptExecutor(argsliceString []string, remoteVersion *semver.Version, localVersion *semver.Version) {

newVersion := fmt.Sprint(remoteVersion)
previousVersion := fmt.Sprint(localVersion)
argslice := append(argsliceString, newVersion, previousVersion)
scriptParams := append([]string{"./autoupdate-script.sh"}, argslice...)
Info("Autoupdater - New version ", newVersion)
Info("Autoupdater - Existing Version ", previousVersion)
Info("Autoupdater - Executing shell script to upgrade go app.Passing parameters ", scriptParams) // ./autoupdate-script.sh goexecutable 7.1.0 7.0.0
cmd := exec.Command("sudo", scriptParams...)
err := cmd.Start()
if err!=nil{
    Info(err)
}
cmd.Process.Release()

}

Posts on "how to run a script in Golang and disown/detach it" have been posted but could not find any posts on this type of an issue. Please help me resolve the issue or correct me if there are any errors in my understanding.

achilles
  • 536
  • 5
  • 16

2 Answers2

1

invokes an autoupdate bash script present in the same working directory as the executable.

I would recommend running it with systemd-run. Excerpt from the man-page:

It will run in a clean and detached execution environment.

Fabian
  • 5,040
  • 2
  • 23
  • 35
  • systemd-run forces me to use absolute paths in the script and I feel it is not the right way to proceed. It doesnt seem to have the "--working-dir" option like systemctl – achilles Jul 10 '19 at 07:00
  • [--working-directory](https://www.freedesktop.org/software/systemd/man/systemd-run.html#--working-directory=) is documented in the man-page and does not seem to work, at least not in my case. You can give the script the current working directory path as an argument and have it cd there before doing anything else. – Fabian Jul 10 '19 at 07:12
  • Fabian thank you for the solution. systemd-run does solve the problem but does have a few drawbacks. I'm currently looking into how I can bring the logs of the systemd-run unit and systemctl service under same unit. – achilles Jul 15 '19 at 05:44
  • @achilles I do not know how you would tie two together. But another solution could be to make the updater the dominant process who starts the Golang app and messages it with an Interrupt-Signal if it should shut down. Then wait for it to shut down, update the binary and start it up again, all from the updater systemd unit. Or you could try [KillMode](https://stackoverflow.com/questions/32208782/cant-detach-child-process-when-main-process-is-started-from-systemd) to not have the updater die? – Fabian Jul 15 '19 at 08:17
  • 1
    Fabian I was able to tie the two together using the command : journalctl _SYSTEMD_UNIT=cantizconnect.service + SYSLOG_IDENTIFIER=cantizconnect – achilles Jul 16 '19 at 12:08
  • @achilles nice! – Fabian Jul 16 '19 at 14:22
0

Hi this is a bit old post but still something that took my interest because we are currently developing a standalone single binary webapp with go that will be running in client's machines and we are planning to implement auto update&upgrade capability to the application before releasing the v1.0.0.

So I was doing some research about the topic to collect all readables to develop the idea if there is something required.

If you are still around, can you update us how this project is going and is it sustainable and maintanable to implement this feature?

Secondly, what we planning to do is;

  • Use a private gitlab repo as a version control, so whenever we are ready we can kick a CD&CI pipeline that runs a Makefile to build both front-end and back-end by doing some other magical stuff to come up with a single binary artifact. Its working that way so far.

  • Secondly, we having a clients DB that we store what versions clients are on. Whenever we do a new version build via gitlab CD&CI, we planning to set new version to a niche of a clients as a pilot group.

  • Our app has a secondary thread running in the background and checking the version from DB on hourly basis and whenever it detects a version > current it notifies the front-end admin user to click and upgrade the system (this is to not to interrupt client in the middle of something)

  • Now the fun part and non-implemented part comes: Plan is to donwload the new binary with wget or something similar as golang library from gitlab, to a temporary location and do some sanity checks there to make sure app is OK. Then replace the current binary with new binary and send a service <name> restart command to the systemd service to restart the application, so that new features would be implemented since it will run from the new binary.

Up until now this seems to be applicable, and smooth. Is there something else you would like to point from your experience?

And also the reason I'm posting this is; This might be another (maybe smoother) way of implementing self-updating application since this is having minimal interraction with the OS without any shell or bash scripts to be ran other than service restart command at the end of all.

Güney Saramalı
  • 791
  • 1
  • 10
  • 19