Don't Force It,
Get a Bigger Hammer!

There are official was to do things in Linux, then there are the Big Hammers to clobber stuff behind the scenes. In my experience the Big Hammer is always the best technique, because some OCD Linux perfectionist is always screwing with the official way to make it better (where the definition of better never seems to include documented or backward compatible as a subset).

Modifying a system with the Big Hammer technique often means modifying or removing files which packages install, but don't expect will be modified by the user. As a user, I have my own opinion about what I need to modify, thus was born the afteryum plugin to restrike all my hammer blows following an update.

Since dnf replaces yum in fedora 22, I now need a more complicated afterdnf plugin.

afterdnf files

/usr/lib/python2.7/site-packages/dnf-plugins/afterdnf.py

# Big hammer DNF plugin that uses __init__ to find the pid of dnf, then
# backgrounds a script to wait for the DNF pid to exit so it can run
# arbitrary stuff following dnf. I have absolutely no idea what most of this
# gibberish is, I'm just copying an existing plugin as best I can and
# getting the hell out of python and into shell scripts as fast as
# possible. Using strace on dnf it appears as though the path I should
# install this in is:
#
# /usr/lib/python2.7/site-packages/dnf-plugins/
#
# (certainly the official dnf docs desperately avoid actually
# saying anything about the real path, so I had to go with strace :-).

from __future__ import absolute_import
from __future__ import unicode_literals

import dnf
import dnfpluginscore
import os

_ = dnfpluginscore._

class AfterDnf(dnf.Plugin):

   name = 'afterdnf'

   def __init__(self, base, cli):
      super(AfterDnf, self).__init__(base,cli)
      os.system("/usr/local/bin/after-dnf-start {0}".format(os.getpid()))

That runs this script as soon as dnf plugins are loaded (passing it the pid of the dnf process as an argument):

/usr/local/bin/after-dnf-start

#!/bin/sh
#
# This script is invoked by the "afterdnf" plugin as soon as dnf
# starts. It gets 1 argument: the PID of the dnf process. The job
# of this script is to background another script which will wait
# for that pid to exit, then run the real after dnf hooks.
#
# This seems extreme, but running the background job in another
# login session helps make it be really, really, backgrounded
# and not associated with the original command at all in any
# way which would encourage anyone to wait on it even though
# it is backgrounded :-).
#
su -l root /bin/bash -c \
   "/usr/local/bin/bg-dammit /bin/bash /usr/local/bin/after-dnf-wait $1"

Since I want to wait till dnf is finished, I have to background a different script to wait for the pid to exit. I use this bg-dammit program to do that (since it seems more reliable than nohup):

bg-dammit.c

// The nohup program seems to let things kill its kids in fedora 20.
// This program will endeavor to eradicate whatever is leaking through
// nohup.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>

int bastards[] = {
   SIGHUP, SIGINT, SIGQUIT, SIGABRT, SIGFPE, SIGPIPE, SIGTERM,
   SIGUSR1, SIGUSR2
};

#ifdef DO_DEBUG

#define DBPRINTF(args) \
   { \
      FILE * dbf = fopen("/home/tom/dammit.log", "a"); \
      fprintf args; \
      fclose(dbf); \
   }

#else

#define DBPRINTF(args)

#endif

int
main(int argc, char ** argv) {

   // Make everything point to /dev/null, close all other file descriptors

   int null_out_fd = open("/dev/null", O_WRONLY);
   int null_in_fd = open("/dev/null", O_RDONLY);
   dup2(null_out_fd, fileno(stdout));
   dup2(null_out_fd, fileno(stderr));
   dup2(null_in_fd, fileno(stdin));
   for (int i = 3; i < 60000; ++i) {
      close(i);
   }

   // Ignore every ignorable signal

   struct sigaction act;
   for (int i = 0; i < sizeof(bastards)/sizeof(bastards[0]); ++i) {
      memset((void *)&act, 0, sizeof(act));
      act.sa_handler = SIG_IGN;
      sigaction(bastards[i], &act, NULL);
   }

   // Fork a child we can make into a new session

   pid_t kid = fork();
   if (kid == 0) {
      // This is the child. Make a new session here.
      pid_t s = setsid();
      DBPRINTF((dbf,"1st setsid call returns %d\n",(int)s))
      // Now fork yet another child to do all the real work and make
      // the intermediate child exit.
      pid_t grandkid = fork();
      if (grandkid == 0) {
         s = setsid(); // Just to make doubly sure.
         DBPRINTF((dbf,"2nd setsid call returns %d\n",(int)s))
         ++argv;
         --argc;
         execvp(argv[0], argv);
         DBPRINTF((dbf,"Oops. Executed past the exec of %s\n",argv[0]))
         _exit(2);
      } else {
         _exit(0);
      }
   } else {
      // This is the parent, wait for the kid.
      int status;
      waitpid(kid, &status, 0);
   }
   return 0;
}

I compile that with gcc and install the executable in /usr/local/bin/bg-dammit. That is then used to background this script:

/usr/local/bin/after-dnf-wait

#!/bin/bash
#
# This script is put in the background by after-dnf-start and gets
# the pid of the dnf command as an argument. It waits for the dnf
# command to exit then runs the after-dnf-hooks script which contains
# any user defined commands desired to be run following a dnf update.
#
# I don't know if it is necessary here, but my udev scripts weren't
# fully backgrounded till I turned them into "user" tasks, so I'll
# do the same here just to be paranoid.

echo $$ > /sys/fs/cgroup/systemd/user.slice/tasks

# Wait for the dnf command to exit

if /usr/local/bin/wait-for-pid "$1"
then
   /usr/local/bin/after-dnf-hooks
fi

That script, now safely running in the background can use this to wait for the original dnf pid to exit:

/usr/local/bin/wait-for-pid

#!/bin/sh
#
# usage: wait-for-pid pid
#
# waits for /proc/pid to disappear from the /proc filesystem
#
if [ $# -ne 1 ]
then
   echo Run wait-for-pid with one PID argument
   exit 2
fi
pid="$1"
numpid=`echo $pid | sed -e 's/[^0-9]//g'`
if [ "$numpid" != "$pid" ]
then
   echo pid $pid must be all decimial digits
   exit 2
fi
if [ -d /proc/$pid ]
then
   while [ -d /proc/$pid ]
   do
      sleep 1
   done
fi

Once the pid is gone, at long last it finally runs the /usr/local/bin/after-dnf-hooks script where you can put anything you want to happen after dnf runs. Mine looks a lot like the after-yum-hook script below.

afteryum files

Install these files on your system (assuming your system uses yum and rpms for updates, I tend to do everything on Fedora, so this works for me):

/usr/lib/yum-plugins/afteryum.py

#!/usr/bin/python

# Install this in /usr/lib/yum-plugins/afteryum.py
# also install afteryum.conf in /etc/yum/pluginconf.d/

import os
from yum.plugins import PluginYumExit, TYPE_INTERACTIVE

requires_api_version = '2.1'
plugin_type = (TYPE_INTERACTIVE,)

def close_hook(conduit):
    os.system('/usr/local/bin/after-yum-hook')

/etc/yum/pluginconf.d/afteryum.conf

[main]
enabled=1

Magically, the /usr/local/bin/after-yum-hook script will now run at the end of any yum command (or yum transactions run from other update tools).

This plugin is not very elegant. Python is not a game I have learned to play well, so this plugin is the bare minimum I could get working by goolgling yum plugins. It does work well enough for my purposes though.

Breaking News! It always seemed like this should be a plugin someone had already written, and I finally found an answer when asking about it in this Fedora Users mailing list message.

It seems I should switch to the officially supported yum-plugin-post-transaction-actions plugin (what a name!). (Of course this wasn't ported to dnf, so it is just as well I never got around to switching to it).

common files

You can put all kinds of stuff in your own after-yum-hook (or after-dnf-hooks) script, but here is mine:

/usr/local/bin/after-yum-hook

#!/bin/bash
#
exec >> /var/log/afteryum.log 2>&1
date
/usr/local/bin/fix-anacron
/usr/local/bin/fix-logwatch
/usr/local/bin/fix-Adwaita
/usr/local/bin/fix-helpful
/usr/local/bin/fix-keyring
/usr/local/bin/fix-chrome

So what do all the additional scripts called from after-yum-hook do for me?

I find anacron to be one of the most annoying pieces of software ever invented. Instead of running background tasks at predictable times when they won't interfere with what you are doing, it runs them at random times and seems to be psychic because it always picks the worst possible time to run them.

Worse than that, anacron was once a separate package you could just remove if you didn't want it. Then it got “improved” by being integrated with cron (so this is a good specific example of why the Big Hammer can be better).

This script uses the Big Hammer to eradicate every trace of anacron. Erasing the anacrontab file that tells anacron it has things to do and the cron.hourly anacron job that triggers anacron from cron.

I am then free to move all the normal cron processing back to the /etc/crontab file where it belongs, and will run at regular, predictable times.

/usr/local/bin/fix-anacron

#!/bin/bash
#
# Drive a stake through anacron's heart if updates try to
# reinstall it.
#
for i in /etc/cron.d/0hourly /etc/cron.hourly/0anacron /etc/anacrontab
do
   if [ -s $i ]
   then
      cat /dev/null > $i
   fi
   rm -f $i.rpmnew
   rm -f $i.rpmsave
done

logwatch is a handy tool that scans logs for important messages and mails me a summary every day. It has billions of arcane rules for deciding on what important means, stashed in the perl scripts that drive the logwatch processing.

One or two of those rules could be improved. I really don't care how often the dovecot imap server stores a mail message (that is, after all, the job it is supposed to do, not some abnormal operation). I also don't care about names the DNS server couldn't lookup. Web pages often have bad names in them.

So, I send a perl script to fix a perl script:

/usr/local/bin/fix-logwatch

#!/usr/bin/perl
#
# Fix the logwatch patterns to get rid of annoying multiple messages
# that really should be merged.

use File::Compare;
use File::Copy;

# Step 1 - find the dovecot script file location from the
# logwatch package file list.
#
my $fh;
my $infile;
open($fh, '-|', '/bin/rpm', '-q', '--list', 'logwatch');
while (<$fh>) {
   if (/\/dovecot$/) {
      chomp;
      $infile=$_;
      last;
   }
}
close($fh);
undef $fh;
#
# Step 2 - copy file to temp, looking for first "while" statement and
# adding a "next if" to skip all lines that look like the annoying
# dovecot.*stored mail into mailbox messages I never want to see.
#
my $outfile = "/tmp/dovecot$$";
my $outf;
open($fh, '<', $infile) || die "Cannot read $infile : $!\n";
open($outf, '>', $outfile) || die "Cannot write $outfile : $!\n";
my $found=0;
while (<$fh>) {
   if (/^\s*while.*ThisLine.*$/) {
      $found=1;
      last;
   } else {
      print $outf $_;
   }
}
if (! $found) {
   close($outf);
   unlink($outfile);
   die "Did not find while ... ThisLine definition!\n";
}
print $outf $_;
print $outf
   '   next if ($ThisLine =~ /^.*dovecot.*stored mail into mailbox.*$/);', "\n";
while (<$fh>) {
   next if (/^.*next if.*dovecot.*stored mail into mailbox.*$/);
   print $outf $_;
}
close($fh);
close($outf);
if (compare($infile, $outfile) != 0) {
   # Looks like the files are different, I really need to fix the
   # function. Copy the outfile back over the top of the infile.
   print "Updating $infile\n";
   copy($outfile, $infile);
}
unlink($outfile);

# Repeat the same thing on the named script...

# Step 1 - find the named script file location from the
# logwatch package file list.
#
undef $fh;
undef $infile;
open($fh, '-|', '/bin/rpm', '-q', '--list', 'logwatch');
while (<$fh>) {
   if (/\/named$/) {
      chomp;
      $infile=$_;
      last;
   }
}
close($fh);
undef $fh;
#
# Step 2 - copy file to temp, looking for first "while" statement and
# adding a "next if" to skip all lines that look like the annoying
# DNS lookup errors I never want to see.
#
$outfile = "/tmp/named$$";
undef $outf;
open($fh, '<', $infile) || die "Cannot read $infile : $!\n";
open($outf, '>', $outfile) || die "Cannot write $outfile : $!\n";
$found=0;
while (<$fh>) {
   if (/^\s*while.*ThisLine.*$/) {
      $found=1;
      last;
   } else {
      print $outf $_;
   }
}
if (! $found) {
   close($outf);
   unlink($outfile);
   die "Did not find while ... ThisLine definition!\n";
}
print $outf $_;
print $outf
   '   next if ($ThisLine =~ /^.*DNS format error from.*$/);', "\n";
print $outf
   '   next if ($ThisLine =~ /^.*error.*FORMERR.*resolving.*$/);', "\n";
while (<$fh>) {
   next if (/^.*next if.*DNS format error from.*$/);
   next if (/^.*next if.*error.*FORMERR.*resolving.*$/);
   print $outf $_;
}
close($fh);
close($outf);
if (compare($infile, $outfile) != 0) {
   # Looks like the files are different, I really need to fix the
   # function. Copy the outfile back over the top of the infile.
   print "Updating $infile\n";
   copy($outfile, $infile);
}
unlink($outfile);

The default gnome theme in recent versions of Fedora is named Adwaita. I think perhaps that is an ancient Etruscan word meaning “Nearly Invisible Selection”. Items that are merely highlighted stand out like a sore thumb, but the item you actually selected is in a color that is just barely different than the surrounding background. This had to be the work of a committee, no one individual could be so moronic.

Anyway, this script digs up the files where the theme definitions are stored (there are two of them - one for gtk2 and one for gtk3) and wacks them on the head to change the selection color to the one I have recognized as the selection for years and years before Adwaita showed up.

/usr/local/bin/fix-Adwaita

#!/bin/bash
#
# Fix the selection color in the default Adwaita theme
#
PATH="/bin:/usr/bin:/sbin:/usr/sbin"
export PATH
TMP="/tmp/Adwaita$$"
mkdir -p "$TMP"
trap "rm -rf $TMP" EXIT
f1="/usr/share/themes/Adwaita/gtk-3.0/settings.ini"
f2="/usr/share/themes/Adwaita/gtk-2.0/gtkrc"
sed -e 's/selected_bg_color:#[0-9a-fA-F][0-9a-fA-F]*/selected_bg_color:#008dd7/' \
   < "$f1" > "$TMP/t1"
if cmp -s "$f1" "$TMP/t1"
then
   :
else
   echo Updating $f1
   cp -f "$TMP/t1" "$f1"
fi
sed -e 's/selected_bg_color:#[0-9a-fA-F][0-9a-fA-F]*/selected_bg_color:#008DD7/' \
   < "$f2" > "$TMP/t2"
if cmp -s "$f2" "$TMP/t2"
then
   :
else
   echo Updating $f2
   cp -f "$TMP/t2" "$f2"
fi
#
# Also fix tooltip timeout so they don't popup incessantly
#
# These files changed location between f19 and f20:
#
newfile="/usr/share/gtk-2.0/gtkrc"
[ -f "$newfile" ] || newfile="/etc/gtk-2.0/gtkrc"
if [ -f "$newfile" ]
then
   fgrep -v gtk-tooltip-timeout < "$newfile" > "$TMP/etcgtkrc"
   echo "gtk-tooltip-timeout = 10000" >> "$TMP/etcgtkrc"
   if cmp -s "$newfile" "$TMP/etcgtkrc"
   then
      :
   else
      echo Updating "$newfile"
      cp -f "$TMP/etcgtkrc" "$newfile"
   fi
else
   echo 'Hey, gtkrc moved again!'
fi
newfile="/usr/share/gtk-3.0/settings.ini"
[ -f "$newfile" ] || newfile="/etc/gtk-3.0/settings.ini"
if [ -f "$newfile" ]
then
   fgrep -v gtk-tooltip-timeout < "$newfile" > "$TMP/etcsettings.ini"
   echo "gtk-tooltip-timeout = 10000" >> "$TMP/etcsettings.ini"
   if cmp -s "$newfile" "$TMP/etcsettings.ini"
   then
      :
   else
      echo Updating "$newfile"
      cp -f "$TMP/etcsettings.ini" "$newfile"
   fi
else
   echo 'Hey, settings.ini moved again!'
fi

The final script simply eradicates a number of annoying “helpful” features installed by various packages.

The /etc/profile.d/color* and /etc/profile.d/vim* files are the source of the annoying garish colors that things like ls, grep, and vi flash all over my terminal windows. They are especially annoying since I like a dark background and light foreground in my terminal, and the colors these monstrosities use are close to impossible to read on a dark background.

Getting rid of the /etc/profile.d/PackageKit* files is important for eradicating the long delays and unhelpful advice about installing missing programs that you get simply because you made a typo in the name of a program you tried to run in the shell.

The most important one is /etc/profile.d/*completion*. After expecting TAB to complete filenames for years and years, these annoying files introduce some sort of bastardized expert system which thinks it knows what you want completion to do in any given context, but is, in fact, always wrong. Removing these files restores sane behavior you can depend upon.

/usr/local/bin/fix-helpful

#!/bin/bash
#
# Rid the world of annoying "helpful" features
#
rm -f /etc/profile.d/color* /etc/profile.d/PackageKit* /etc/profile.d/vim*
rm -f /etc/profile.d/*completion*
#
# qemu updates continue to "helpfully" re-enable these services, so just
# keep turning them off after every update...
#
# Apparently these don't exist any longer, don't bother with this.
#
# systemctl disable ksm.service
# systemctl disable ksmtuned.service

As a bonus, here is a sample script I once ran from the after-yum-hook, but decided to do another way:

This script modifies a default xulrunner script to ignore the autocomplete disable attribute snooty webmasters put on their web sites because they have sites that are far too good to allow a dirty firefox browser to remember their precious passwords.

While I don't often agree with the snooty web admins, sometimes I do, which makes this hammer too big even for me. I found another way to fix this with the greasemonkey Autocomplete On script I can configure to work only on specific sites.

/usr/local/bin/fix-firefox

#!/usr/bin/perl
#
# Hack firefox to believe that no web page contains an autocomplete disable
# attribute (that will teach stuck up web admins to think their passwords
# are too good to be saved in firefox :-).

use File::Compare;
use File::Copy;

# Step 1 - find the nsLoginManager.js file location from the
# xulrunner package file list.
#
my $fh;
my $infile;
open($fh, '-|', '/bin/rpm', '-q', '--list', 'xulrunner');
while (<$fh>) {
   if (/\/nsLoginManager\.js$/) {
      chomp;
      $infile=$_;
      last;
   }
}
close($fh);
undef $fh;
#
# Step 2 - copy file to temp, replacing the definition of the
# _isAutocompleteDisabled function with one which always returns false.
#
my $outfile = "/tmp/nslm$$.js";
my $outf;
open($fh, '<', $infile) || die "Cannot read $infile : $!\n";
open($outf, '>', $outfile) || die "Cannot write $outfile : $!\n";
$found=0;
while (<$fh>) {
   if (/^\s*_isAutocompleteDisabled\s*:\s*function.*\{\s*$/) {
      $found=1;
      last;
   } else {
      print $outf $_;
   }
}
if (! $found) {
   close($outf);
   unlink($outfile);
   die "Did not find _isAutocompleteDisabled function definition!\n";
}
print $outf $_;
$found=0;
while (<$fh>) {
   if (/^\s*return\s+false\s*\;\s*$/) {
      $found=1;
      last;
   }
}
if (! $found) {
   close($outf);
   unlink($outfile);
   die "Did not find return false statement!\n";
}
print $outf $_;
$found=0;
while (<$fh>) {
   if (/^\s*\}/) {
      $found=1;
      last;
   }
}
if (! $found) {
   close($outf);
   unlink($outfile);
   die "Did not find closing squiggly brace!\n";
}
print $outf $_;
while (<$fh>) {
   print $outf $_;
}
close($fh);
close($outf);
if (compare($infile, $outfile) != 0) {
   # Looks like the files are different, I really need to fix the
   # function. Copy the outfile back over the top of the infile.
   print "Updating $infile\n";
   copy($outfile, $infile);
}
unlink($outfile);
 
Game of Linux Entry Game of Linux Site Map Tom's Fabulous Web Page
 
Page last modified Sun Jul 19 18:54:56 2015