How to log calls using a wrapper script when there are multiple symlinks to the executable

You can use exec -a (as found in bash, ksh93, zsh, mksh, yash but not POSIX yet) that is used to specify an argv[0] to a command being executed:

#! /bin/bash -
printf '%s\n' "$0 $*" >> /some/log
exec -a "$0" /usr/bin/do_stuff_real "$@"

Note that that $0 is not the argv[0] that the command receives. It is the path of the script as passed to execve() (and which is passed as argument to bash), but that's likely to be enough for your purpose.

As an example, if make_tea was invoked as:

execv("/usr/bin/make_tea", ["make_tea", "--sugar=2"])

As a shell would typically do when invoking the command by name (looking up the executable in $PATH), the wrapper would to:

execv("/usr/bin/do_stuff_real", ["/usr/bin/make_tea", "--sugar=2"])

That's not:

execv("/usr/bin/do_stuff_real", ["make_tea", "--sugar=2"])

but that's good enough as do_stuff_real knows it's meant to make tea.

Where that would be a problem is if do_stuff was invoked as:

execv("/usr/bin/do_stuff", ["/usr/bin/make_tea", "--sugar=2"])

as that would be translated to:

execv("/usr/bin/do_stuff_real", ["/usr/bin/do_stuff", "--sugar=2"])

That would not happen during normal operations, but note that our wrapper does something like that.

On most systems, the argv[0] as passed to the script is lost once the interpreter (here /bin/bash) is executed (the argv[0] of the interpreter on most systems is the path given on the she-bang line) so there's nothing a shell script can do about it.

If you wanted to pass the argv[0] along, you'd need to compile an executable. Something like:

#include <stdio.h>
int main(int argc, char *argv[], char *envp[])
{
   /* add logging */
   execve("/usr/bin/do_stuff_real", argv, envp);
   perror("execve");
   return 127;
}

You often see this in case of utilities like busybox, a program that can provide most of the common unix utilities in one executable, that behaves different depending on its invocation/ busybox can do a whole lot of functions, acpid through zcat.

And it commonly decides what it's supposed to be doing by looking at it's argv[0] parameter to main(). And that shouldn't be a simple comparison. Because argv[0] might be something like sleep, or it might be /bin/sleep and it should decide to do the same thing. In other words, the path is going to make things more complex.

So if things were done by the worker program right, your logging wrapper could execute from something like /bin/realstuff/make_tea and if the worker looks at argv[0] basename only, then the right function should execute.

#!/bin/sh -
myexec=/tmp/MYEXEC$$
mybase=`basename -- "$0"`

echo "$0 $@" >> logfile

mkdir "$myexec" || exit
ln -fs /usr/bin/real/do_stuff "$myexec/$mybase" || exit
"$myexec/$mybase" "$@"
ret=$?
rm -rf "$myexec"
exit "$ret"

In the example above, argv[0] should read something like /tmp/MYEXEC4321/make_tea (if 4321 was the PID for the /bin/sh that ran)which should trigger the basename make_tea behavior

If you want argv[0] to be an exact copy of what it would be without the wrapper, you have a tougher problem. Because of absolute file paths beginning with /. You can't make a new /bin/sleep (absent chroot and I don't think you want to go there). As you note, you could do that with some flavor of exec(), but it wouldn't be a shell wrapper.

Have you considered using an alias to hit the logger and then start the base program instead of a script wrapper? It'd only catch a limited set of events, but maybe those are the only events you care about

Tags:

Shell Script