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?
In fedora 38 developers have gotten on a pointlessly destroying
backward compatibility kick, so these scripts help make sure
uname, fgrep, and egrep keep working no matter how crazy the
developers get:
/usr/local/bin/fix-uname
#!/bin/bash
#
# The OCD onslaught continues with uname -p and uname -i now returning
# the string "unknown" instead of the architecture. Satisfying their
# OCD will only require thousands of people to modify decades old
# shell scripts, so obviously it is a good idea to change this.
#
# This script checks to see if uname is broken, and if it is, moves
# it to uname.real and copies in the /zooty/uname/uname shell
# script which replaces a single -p or -i option with -m. (Easier
# than a general solution and should be the 99.999% case).
#
unk=`/usr/bin/uname -p`
gud=`/usr/bin/uname -m`
if [ "$unk" != "$gud" ]
then
rm -f /usr/bin/uname.real
mv /usr/bin/uname /usr/bin/uname.real
cp /zooty/uname/uname /usr/bin/uname
chmod 755 /usr/bin/uname
fi
The uname script that installs to replace the uname executable
looks like:
/zooty/uname/uname
#!/bin/bash
#
# Replacement for uname command that changes a single -p or -i option
# into a -m option, otherwise passes everything to real uname program
#
if [ "$#" -eq "1" ]
then
if [ "$1" = "-p" ]
then
exec /usr/bin/uname.real -m
fi
if [ "$1" = "-i" ]
then
exec /usr/bin/uname.real -m
fi
fi
exec /usr/bin/uname.real "$@"
Fix egrep and grep with these:
/usr/local/bin/fix-grep
#!/bin/bash
#
# Note that the test command will get errors if the stupid new version
# is there or if they completely remove the programs, so I'll get a
# proper version copied back in either case.
#
greptest=`/usr/bin/fgrep booger /dev/null 2>&1`
if [ -n "$greptest" ]
then
rm -f /usr/bin/fgrep
cp /zooty/grep/fgrep /usr/bin/fgrep
chmod 755 /usr/bin/fgrep
fi
greptest=`/usr/bin/egrep booger /dev/null 2>&1`
if [ -n "$greptest" ]
then
rm -f /usr/bin/egrep
cp /zooty/grep/egrep /usr/bin/egrep
chmod 755 /usr/bin/egrep
fi
The versions of egrep and fgrep are just the scripts that
were already in use in fedora 37 with no stupid stderr message:
/zooty/grep/egrep
#!/usr/bin/sh
exec grep -E "$@"
/zooty/grep/fgrep
#!/usr/bin/sh
exec grep -F "$@"
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);
|