Subject: Re: [RFC PATCH 3/5] maintenance: use packaged systemd units

From: Patrick Steinhardt <[email-masked]>
Date: Thu, 21 Mar 2024 13:37:41 +0100

On Mon, Mar 18, 2024 at 04:31:17PM +0100, Max Gautier wrote:
> - Remove the code writing the units
> - Simplify calling systemctl
>   - systemctl does not error out when disabling units which aren't
>     enabled, nor when enabling already enabled units
>   - call systemctl only once, with all the units.
> - Add clean-up code for leftover units in $XDG_CONFIG_HOME created by
>   previous versions of git, taking care of preserving actual user
>   override (by checking the start of the file).

We very much try to avoid bulleted-list-style commit messages. Once you
have a list of changes it very often indicates that there are multiple
semi-related things going on in the same commit. At that point we prefer
to split the commit up into multiple commits so that each of the bullet
items can be explained separately.

Also, as mentioned before, commit messages should explain what the
problem is and how that problem is solved. 

Patrick

> Signed-off-by: Max Gautier <[email-masked]>
> ---
>  builtin/gc.c | 293 ++++++++-------------------------------------------
>  1 file changed, 45 insertions(+), 248 deletions(-)
> 
> diff --git a/builtin/gc.c b/builtin/gc.c
> index cb80ced6cb..981db8e297 100644
> --- a/builtin/gc.c
> +++ b/builtin/gc.c
> @@ -2308,276 +2308,73 @@ static char *xdg_config_home_systemd(const char *filename)
>  	return xdg_config_home_for("systemd/user", filename);
>  }
>  
> -#define SYSTEMD_UNIT_FORMAT "git-maintenance@%s.%s"
> -
> -static int systemd_timer_delete_timer_file(enum schedule_priority priority)
> -{
> -	int ret = 0;
> -	const char *frequency = get_frequency(priority);
> -	char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer");
> -	char *filename = xdg_config_home_systemd(local_timer_name);
> -
> -	if (unlink(filename) && !is_missing_file_error(errno))
> -		ret = error_errno(_("failed to delete '%s'"), filename);
> -
> -	free(filename);
> -	free(local_timer_name);
> -	return ret;
> -}
> -
> -static int systemd_timer_delete_service_template(void)
> -{
> -	int ret = 0;
> -	char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service");
> -	char *filename = xdg_config_home_systemd(local_service_name);
> -	if (unlink(filename) && !is_missing_file_error(errno))
> -		ret = error_errno(_("failed to delete '%s'"), filename);
> -
> -	free(filename);
> -	free(local_service_name);
> -	return ret;
> -}
> -
> -/*
> - * Write the schedule information into a git-maintenance@<schedule>.timer
> - * file using a custom minute. This timer file cannot use the templating
> - * system, so we generate a specific file for each.
> - */
> -static int systemd_timer_write_timer_file(enum schedule_priority schedule,
> -					  int minute)
> -{
> -	int res = -1;
> -	char *filename;
> -	FILE *file;
> -	const char *unit;
> -	char *schedule_pattern = NULL;
> -	const char *frequency = get_frequency(schedule);
> -	char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer");
> -
> -	filename = xdg_config_home_systemd(local_timer_name);
> -
> -	if (safe_create_leading_directories(filename)) {
> -		error(_("failed to create directories for '%s'"), filename);
> -		goto error;
> -	}
> -	file = fopen_or_warn(filename, "w");
> -	if (!file)
> -		goto error;
> -
> -	switch (schedule) {
> -	case SCHEDULE_HOURLY:
> -		schedule_pattern = xstrfmt("*-*-* 1..23:%02d:00", minute);
> -		break;
> -
> -	case SCHEDULE_DAILY:
> -		schedule_pattern = xstrfmt("Tue..Sun *-*-* 0:%02d:00", minute);
> -		break;
> -
> -	case SCHEDULE_WEEKLY:
> -		schedule_pattern = xstrfmt("Mon 0:%02d:00", minute);
> -		break;
> -
> -	default:
> -		BUG("Unhandled schedule_priority");
> -	}
> -
> -	unit = "# This file was created and is maintained by Git.\n"
> -	       "# Any edits made in this file might be replaced in the future\n"
> -	       "# by a Git command.\n"
> -	       "\n"
> -	       "[Unit]\n"
> -	       "Description=Optimize Git repositories data\n"
> -	       "\n"
> -	       "[Timer]\n"
> -	       "OnCalendar=%s\n"
> -	       "Persistent=true\n"
> -	       "\n"
> -	       "[Install]\n"
> -	       "WantedBy=timers.target\n";
> -	if (fprintf(file, unit, schedule_pattern) < 0) {
> -		error(_("failed to write to '%s'"), filename);
> -		fclose(file);
> -		goto error;
> -	}
> -	if (fclose(file) == EOF) {
> -		error_errno(_("failed to flush '%s'"), filename);
> -		goto error;
> -	}
> -
> -	res = 0;
> -
> -error:
> -	free(schedule_pattern);
> -	free(local_timer_name);
> -	free(filename);
> -	return res;
> -}
> -
> -/*
> - * No matter the schedule, we use the same service and can make use of the
> - * templating system. When installing git-maintenance@<schedule>.timer,
> - * systemd will notice that git-maintenance@.service exists as a template
> - * and will use this file and insert the <schedule> into the template at
> - * the position of "%i".
> - */
> -static int systemd_timer_write_service_template(const char *exec_path)
> -{
> -	int res = -1;
> -	char *filename;
> -	FILE *file;
> -	const char *unit;
> -	char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service");
> -
> -	filename = xdg_config_home_systemd(local_service_name);
> -	if (safe_create_leading_directories(filename)) {
> -		error(_("failed to create directories for '%s'"), filename);
> -		goto error;
> -	}
> -	file = fopen_or_warn(filename, "w");
> -	if (!file)
> -		goto error;
> -
> -	unit = "# This file was created and is maintained by Git.\n"
> -	       "# Any edits made in this file might be replaced in the future\n"
> -	       "# by a Git command.\n"
> -	       "\n"
> -	       "[Unit]\n"
> -	       "Description=Optimize Git repositories data\n"
> -	       "\n"
> -	       "[Service]\n"
> -	       "Type=oneshot\n"
> -	       "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n"
> -	       "LockPersonality=yes\n"
> -	       "MemoryDenyWriteExecute=yes\n"
> -	       "NoNewPrivileges=yes\n"
> -	       "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_VSOCK\n"
> -	       "RestrictNamespaces=yes\n"
> -	       "RestrictRealtime=yes\n"
> -	       "RestrictSUIDSGID=yes\n"
> -	       "SystemCallArchitectures=native\n"
> -	       "SystemCallFilter=@system-service\n";
> -	if (fprintf(file, unit, exec_path, exec_path) < 0) {
> -		error(_("failed to write to '%s'"), filename);
> -		fclose(file);
> -		goto error;
> -	}
> -	if (fclose(file) == EOF) {
> -		error_errno(_("failed to flush '%s'"), filename);
> -		goto error;
> -	}
> -
> -	res = 0;
> -
> -error:
> -	free(local_service_name);
> -	free(filename);
> -	return res;
> -}
> -
> -static int systemd_timer_enable_unit(int enable,
> -				     enum schedule_priority schedule,
> -				     int minute)
> +static int systemd_set_units_state(int enable)
>  {
>  	const char *cmd = "systemctl";
>  	struct child_process child = CHILD_PROCESS_INIT;
> -	const char *frequency = get_frequency(schedule);
> -
> -	/*
> -	 * Disabling the systemd unit while it is already disabled makes
> -	 * systemctl print an error.
> -	 * Let's ignore it since it means we already are in the expected state:
> -	 * the unit is disabled.
> -	 *
> -	 * On the other hand, enabling a systemd unit which is already enabled
> -	 * produces no error.
> -	 */
> -	if (!enable)
> -		child.no_stderr = 1;
> -	else if (systemd_timer_write_timer_file(schedule, minute))
> -		return -1;
>  
>  	get_schedule_cmd(&cmd, NULL);
>  	strvec_split(&child.args, cmd);
> -	strvec_pushl(&child.args, "--user", enable ? "enable" : "disable",
> -		     "--now", NULL);
> -	strvec_pushf(&child.args, SYSTEMD_UNIT_FORMAT, frequency, "timer");
> +
> +	strvec_pushl(&child.args, "--user", "--force", "--now",
> +			enable ? "enable" : "disable",
> +			"[email-masked]",
> +			"[email-masked]",
> +			"[email-masked]", NULL);
> +	/*
> +	** --force override existing conflicting symlinks
> +	** We need it because the units have changed location (~/.config ->
> +	** /usr/lib)
> +	*/
>  
>  	if (start_command(&child))
>  		return error(_("failed to start systemctl"));
>  	if (finish_command(&child))
> -		/*
> -		 * Disabling an already disabled systemd unit makes
> -		 * systemctl fail.
> -		 * Let's ignore this failure.
> -		 *
> -		 * Enabling an enabled systemd unit doesn't fail.
> -		 */
> -		if (enable)
> -			return error(_("failed to run systemctl"));
> +		return error(_("failed to run systemctl"));
>  	return 0;
>  }
>  
> -/*
> - * A previous version of Git wrote the timer units as template files.
> - * Clean these up, if they exist.
> - */
> -static void systemd_timer_delete_stale_timer_templates(void)
> +static void systemd_delete_user_unit(char const *unit)
>  {
> -	char *timer_template_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "timer");
> -	char *filename = xdg_config_home_systemd(timer_template_name);
> +	char const	file_start_stale[] =	"# This file was created and is"
> +						" maintained by Git.";
> +	char		file_start_user[sizeof(file_start_stale)] = {'\0'};
> +
> +	char *filename = xdg_config_home_systemd(unit);
> +	int handle = open(filename, O_RDONLY);
>  
> -	if (unlink(filename) && !is_missing_file_error(errno))
> +	/*
> +	** Check this is actually our file and we're not removing a legitimate
> +	** user override.
> +	*/
> +	if (handle == -1 && !is_missing_file_error(errno))
>  		warning(_("failed to delete '%s'"), filename);
> +	else {
> +		read(handle, file_start_user, sizeof(file_start_stale) - 1);
> +		close(handle);
> +		if (strcmp(file_start_stale, file_start_user) == 0) {
> +			if (unlink(filename) == 0)
> +				warning(_("deleted stale unit file '%s'"), filename);
> +			else if (!is_missing_file_error(errno))
> +				warning(_("failed to delete '%s'"), filename);
> +		}
> +	}
>  
>  	free(filename);
> -	free(timer_template_name);
> -}
> -
> -static int systemd_timer_delete_unit_files(void)
> -{
> -	systemd_timer_delete_stale_timer_templates();
> -
> -	/* Purposefully not short-circuited to make sure all are called. */
> -	return systemd_timer_delete_timer_file(SCHEDULE_HOURLY) |
> -	       systemd_timer_delete_timer_file(SCHEDULE_DAILY) |
> -	       systemd_timer_delete_timer_file(SCHEDULE_WEEKLY) |
> -	       systemd_timer_delete_service_template();
> -}
> -
> -static int systemd_timer_delete_units(void)
> -{
> -	int minute = get_random_minute();
> -	/* Purposefully not short-circuited to make sure all are called. */
> -	return systemd_timer_enable_unit(0, SCHEDULE_HOURLY, minute) |
> -	       systemd_timer_enable_unit(0, SCHEDULE_DAILY, minute) |
> -	       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, minute) |
> -	       systemd_timer_delete_unit_files();
> -}
> -
> -static int systemd_timer_setup_units(void)
> -{
> -	int minute = get_random_minute();
> -	const char *exec_path = git_exec_path();
> -
> -	int ret = systemd_timer_write_service_template(exec_path) ||
> -		  systemd_timer_enable_unit(1, SCHEDULE_HOURLY, minute) ||
> -		  systemd_timer_enable_unit(1, SCHEDULE_DAILY, minute) ||
> -		  systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, minute);
> -
> -	if (ret)
> -		systemd_timer_delete_units();
> -	else
> -		systemd_timer_delete_stale_timer_templates();
> -
> -	return ret;
>  }
>  
>  static int systemd_timer_update_schedule(int run_maintenance, int fd UNUSED)
>  {
> -	if (run_maintenance)
> -		return systemd_timer_setup_units();
> -	else
> -		return systemd_timer_delete_units();
> +	/*
> +	 * A previous version of Git wrote the units in the user configuration
> +	 * directory. Clean these up, if they exist.
> +	 */
> +	systemd_delete_user_unit("[email-masked]");
> +	systemd_delete_user_unit("[email-masked]");
> +	systemd_delete_user_unit("[email-masked]");
> +	systemd_delete_user_unit("git-maintenance@.timer");
> +	systemd_delete_user_unit("git-maintenance@.service");
> +	return systemd_set_units_state(run_maintenance);
>  }
>  
>  enum scheduler {
> -- 
> 2.44.0
> 
>