Block/prevent command if it has been executed within the last x seconds/minutes

command

(Just for completion: the story; you don't need to read this if you do not want to know why I am asking this question: I got a webcam demon-application (called "motion") with camera motion detection. This demon is able to trigger a custom command when there is a motion detected by the camera. I furthermore installed a tiny commandline program that easily can send push notifications to my android mobile. I configured the cam-demon to run that push-message-command as he detects a motion.)

The problem: The demon triggers that custom command with every frame he takes for a motion, leading me to a massive amount of push-notification when there is a continuous motion for just – lets say – 5 seconds (the camera framerate is set to 10 pics/second so I get 10 push-msgs every second that is considered a motion).

I need a script or program that block that push-msg command if it has been run in the past x seconds / minutes already.

I could imagine something like:

$ proxy_command.sh --block_time=10s --command==androidpush -msg "There was a motion in your flat!"

I could not find an easy/yet elegant way to solve this (without writing files with timestamps and then checking for that files content). Neither I could find someone having a similar problem.

Is there some kind of command proxy or something that would solve my problem like described above as easy as possible?

Best Answer

Storing the last invocation time as the last modification time of a file

perl

#! /usr/bin/perl
# usage: cmd file seconds cmd args
$file = shift @ARGV;
$time = shift @ARGV;
$age = -M $file;
exit 3 if defined($age) && 86400 * $age < $time;
open $fh, ">>", $file ||
  die "Can't open $file: $!\n";
utime undef, undef, $fh;
exec @ARGV;

To be used like:

that-script /some/file 10 androidpush -msg 'There was a motion in your flat!'

It records the time of last run as the last modification time of /some/file and doesn't run the command if the age of that file is less than the specified time.

shell

With BSD or GNU find, you could do it with:

#! /bin/sh -
file=${1?} time=${2?}
shift 2

[ "$(find -- "$file" -prune -newermt "$time ago")" ] && exit 3
touch -- "$file"
exec "$@"

To run as:

that-script /some/file 10sec androidpush -msg 'There was a motion in your flat!'

In any case, you'll have to store the information about the last run in some place that persists for the next runs. The file system is an obvious place for that. That's one where you can reserve some area for yourself.

sleep process with specific name

Another namespace could be for instance process names:

#! /bin/bash -
label=$0-$(logname)@${1?} time=${2?}
shift 2
pgrep -f "^$label" > /dev/null 2>&1 && exit 3
(exec -a "$label" sleep "$time") &
exec "$@"

To be used as:

that-script motion 10 androidpush -msg 'There was a motion in your flat!'

(assuming a sleep implementation that doesn't care about its argv[0]).

We're using bash instead of sh for its exec -a arg0. If you don't have bash, other shells that support that include ksh93, mksh, yash and zsh. Or you could revert to perl again.

Note that that namespace is not reserved. There's nothing preventing another user to create a process with that same name (as opposed to using a ~/.lastrun file in the file-based approach), however given that here those scripts are all started by the same motion process, you could restrict the search for process to those with the same parent process id:

Improvement for when all the scripts are always started by the same process

#! /bin/bash -
label=$0-${1?} time=${2?}
shift 2
pgrep -P "$PPID" -f "^$label" > /dev/null 2>&1 && exit 3
(exec -a "$label" sleep "$time") &
exec "$@"

Linux-only: use a kernel key with a timeout

On Linux, your could also use a key on the user's session keyring. That has not really be designed for that, but here it comes handy as those keys persist across time and can be given a timeout:

#! /bin/sh -
key=$0-${1?} time=${2?}
shift 2
keyctl search @us user "$key" > /dev/null 2>&1 && exit 3
key=$(keyctl add user "$key" x @us) || exit
keyctl timeout "$key" "$time" || exit
exec "$@"

Now, it doesn't really matter in your case as you're not running two instances of that script at the same time, but all of those have a race conditions which makes that it doesn't guarantee that two instances won't be run within the specified time. If two instances are run at the same time, they could both verify that the condition is OK before any one of them resets it.

Locking a file to avoid the race condition

An approach that would work around that would be to have the sleep process hold a lock on a file:

#! /bin/sh -
file=${1?} time=${2?}
shift 2
{
  flock -n 3 || exit 3
  sleep "$time" 3>&3 &
  exec "$@"
} 3<> "$file"

(in addition here, both sleep and the command being run would hold the lock which would make sure not two instances of the command run at the same time even if they take longer to run than the sleep command).

Related Question