Portable __VA_ARGS__ macros for Linux, HP-UX, Solaris and AIX

I recently needed a macro with variable arguments that work on all mainstream compilers. I use gcc only on Linux, on the other platforms I want to use the „native“ compiler that belongs to the OS. Let’s start with Linux.

Linux

If you’re inside the gnu/gcc world, things are easy. This is what everybody tries first:

1
2
3
4
5
6
7
8
9
10
11
#include "stdio.h"
/* I use "stdio.h" because my blog software cant't handle
   angle brackets inside a preformated text blog ... :( */
 
#define PRINTERROR(x, ...) fprintf(stderr, "ERRORMESSAGE = "  f "\n" , __VA_ARGS__);
 
int main(void)
{
    PRINTERROR("Hello %s!", "world");
    return 0;
}

The problems start, if you want to use the macro without variable arguments.

9
    PRINTERROR("Hello world!");

gcc will not accept the code, because it resolves the macro to

9
    fprintf(stderr, "ERRORMESSAGE = "  "Hello world!" "\n" ,);

(note the comma)

test.c: In function ‘main’:
test.c:9: error: expected expression before ‘)’ token

The gcc workaround for this kind of a situation is:

5
#define PRINTERROR(x, s...) fprintf(stderr, "ERRORMESSAGE = "  f "\n" , ## s);

If „s“ is null, this erases the comma to the left of the „##“ in the macro definition. The resulting expansion is:

9
    fprintf(stderr, "ERRORMESSAGE = "  "Hello world!" "\n" );

The comma is gone, the code compiles. Great.

HP-UX

However, this approach fails on HP-UX

cc -o test test.c
cc: "test.c", line 9: error 1000: Unexpected symbol: ")".

I found the solution for this problem in the HP-docs:

you must insert a space to the left of the comma to make it clear to the preprocessor that the comma is the left operand of the „##“ operator.

Yes. A space. Left of the comma. Sigh….

The definition that works on Linux and HP’s cc -AC99 compiler looks like this:

5
#define PRINTERROR(x, s...) fprintf(stderr, "ERRORMESSAGE = "  x "\n" , ## s);

Solaris

Solaris c89 compiles the above definition with a warning:

"test.c", line 9: warning: argument mismatch

Solaris c99 compiler is very strict and allows

5
#define FOO(...) printf(__VA_ARGS__)

only. The documentation points out, that it is possible to use #__VA_ARGS__ which seems to be similar to GNU’s/HP’s ## Operator:

5
#define PRINTERROR(x, ...) fprintf(stderr, "ERRORMESSAGE = "  x "\n" , # __VA_ARGS__ );

Ok, the code compiles, but calling PRINTERROR results in a „argument mismatch“-warning. I found no other solution than disabling the warning. Dirty but it works:

5
6
7
#define PRINTERROR(x, ...) printf(stderr, "ERRORMESSAGE = "  x "\n" , # __VA_ARGS__ );
/* optional: disables "warning: argument mismatch" */
#pragma error_messages (off, E_ARGUEMENT_MISMATCH)

If course it is bad to hide warnings, but if your code is multi-platform, you can use very strict compiler settings on another platform to make the warnings visible at least there.

AIX

The original AIX-compiler (cc) does not complain, but c89 fails with

"test.c", line 3.16: 1506-1128 (S) The GNU variable argument identifier "s" of macro "FOO" is not permitted in the current langlvl mode.
"test.c", line 7.29: 1506-041 (E) The invocation of macro FOO contains fewer arguments than are required by the macro definition.

There is a langlvl #pragma to switch the compiler language level. All levels that allow implementation-specific language extensions are sufficient. I used extc99 which allows to use the c99 compiler. Simply add this to the beginning of your sourcefile:

#pragma langlvl (extc99)

The final result

This is the result of some hours browsing the compiler docs. The macro works on all c99-compilers mentioned above and allows zero arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifdef _AIX
#pragma langlvl (extc99)
#endif
 
#include "stdio.h"
 
#ifdef _SOLARIS
  #define PRINTERROR(x, ...) fprintf(stderr, "ERRORMESSAGE = "  x "\n" , # __VA_ARGS__ );
  /* optional: disables "warning: argument mismatch" */
  #pragma error_messages (off, E_ARGUEMENT_MISMATCH)
#else
  #define PRINTERROR(x, s...) fprintf(stderr, "ERRORMESSAGE = "  x "\n" , ## s);
#endif
 
int main(void)
{
        PRINTERROR("NO PARAM");
        PRINTERROR("Param: %s", "foo");
 
        return 0;
}

Of course you have to use the compiler switch -D_AIX, -D_SOLARIS or -D_HPUX on the apropriate platform. Let me know what do you think and how it could be done better.

Update: André suggests this solution (see his comment):

void PRINTERROR_verbose(const char * file, int line, int level, char *format, ...) {
	int size;
	char *cp;
    va_list args;
 
	if (level < = 2) {
 
	    va_start(args, format);
    	size = vsnprintf(NULL, 0, format, args) + 1;
    	va_end(args);
 
    	va_start(args, format);
		cp = (char *)calloc(size, sizeof(char));
		if (cp != NULL && vsnprintf(cp, size, format, args) > 0) {
			/* Ausgabe von cp hier via syslog, stderr, stdout, etc. */
			/* oder cp noch mal umformatieren fuer Sonderzeichen und/oder */
			/* zusaetzliche Zeitangabe */
 
			/* bei Debuglevel 1 (z.B. ERROR) wird dann noch etwas mehr angezeigt */
			if (level < = 1) {
				fprintf(stderr, "\tFile '%s' in line %i\n", file, line);
			}
		}
	    va_end(args);
		free(cp);
	}
}
 
#define PRINTERROR(...) PRINTERROR_verbose(__FILE__, __LINE__, __VA_ARGS__)

gdb attach fails with ptrace: Operation not permitted

Today I ran into a weird problem. I could not attach to my own process with gdb. The process ran under my UID, but gdb refused to attach. This is a problem of wrong permissions, although /proc/[pid]/status looked ok:

...
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
...

I am the owner but cannot attach? Well, I launched gdb as root and could attach. Strange. Without digging deeper into this, my dirty workaround was this:

sudo chmod +s /usr/bin/gdb

Update: Thanks to Mario, who pointed out, that the reason is the Kernel hardening stuff build into the Ubuntu kernel. See his comment how to fix the problem permanently.

OpenLDAP Replikation mit syncprov

Für einen neuen Server in unserer Rootserver-Kommune brauche ich eine Replik unseres LDAP-Servers. Wie sich herausstellte, ist das ziemlich einfach und in der umfangreichen Dokumentation exzellent erklärt.

Im zu replizierenden LDAP-Server („Provider“) muss man lediglich dafür sorgen, dass das syncprov-Modul in der slapd.conf geladen wird:

modulepath      /usr/lib/ldap
moduleload      back_hdb
moduleload      syncprov

Im Zielserver („Consumer“), wird so eine 1:1 Replikation des Quellservers konfiguriert:

# replication settings
syncrepl rid=001
    provider=ldaps://ldaps.yourdomain.de:636
    type=refreshAndPersist
    searchbase="dc=yourdomain,dc=de"
    schemachecking=on
    type=refreshAndPersist
    retry="60 +"
    bindmethod=simple
    binddn="cn=replicator,dc=yourdomain,dc=de"
    credentials=assword

Natürlich müssen im Quellserver entsprechende Credentials hinterlegt werden. Durch das Keyword refreshAndPersist bleibt die Verbindung dauerhaft bestehen und Änderungen im Quellserver werden sofort auf dem Zielserver repliziert.

Achtung: im Zielserver müssen natürlich die gleichen Schemata geladen sein, wie im Quellserver. Schaut euch also die Include-Section entsprechend an, sonst hagelt es Fehler.

Mit dieser Methode kann man den LDAP-Server super-komfortabel umziehen, einfach Replikation einschalten, DNS-umschwenken und dann die Replikation auf dem Zielserver ausschalten.

How to enable Server Name Indication (SNI) on Debian Lenny

Server Name Indication (SNI) on Debian Lenny is easy to implement. OpenSSL is already SNI-capable, only the Apache Webserver is a bit outdated. So lets backport Apache >= 2.2.12 to Lenny. Here is my little step-by-step howto:

Install a compiler an the debian dpkg-Development Environment:

$ sudo aptitude install dpkg-dev build-essential fakeroot

To build apache, you will nee libcap2-dev and autoconf too:

$ sudo aptitude install libcap2-dev autoconf

…and the build dependencies for apache2:

$ sudo apt-get build-dep apache2

Download the Apache 2.2.14 sources for the current testing release from http://packages.debian.org/source/squeeze/apache2:

$ wget http://ftp.de.debian.org/debian/pool/main/a/apache2/apache2_2.2.14-1.dsc
$ wget http://ftp.de.debian.org/debian/pool/main/a/apache2/apache2_2.2.14.orig.tar.gz
$ wget http://ftp.de.debian.org/debian/pool/main/a/apache2/apache2_2.2.14-1.diff.gz

Extract the source packages:

$ dpkg-source -x apache2_2.2.14-1.dsc

Compile the package:

$ cd apache2-2.2.14/
$ dpkg-buildpackage -us -uc -rfakeroot

(-us and -uc supresses the digital signature, fakeroot allows to set the ownership of the archived files to root, even if you are not root currently)

Install the desired apache packets:

$ cd ../
$ dpkg -i apache2_2.2.14-1_i386.deb apache2.2-bin_2.2.14-1_i386.deb apache2.2-common_2.2.14-1_i386.deb apache2-mpm-prefork_2.2.14-1_i386.deb apache2-suexec_2.2.14-1_i386.deb apache2-utils_2.2.14-1_i386.deb

Finally, remove the packages you installed to build the apache2-packages.

Surfterminal mit Linux

Wenn man einen Rechner im öffentlichen Raum als Browserterminal bereitstellen will, gibt es hauptsächlich die Anforderung, dass die User keine privaten Daten hinterlassen und am System so wenig wie möglich verstellen.

Es gibt verschiedenste Lösungen für diesen Zweck, fertige Linux-Distributionen oder spezielle Internet-Café Software, die Windows-Rechner so abschottet, das man noch nicht mal einen Explorer aufbekommt.

Für die beiden vom Freifunk betriebenen Surfterminals habe ich eine extrem simple Lösung gewählt. Die Rechner verrichten seit etlichen Monaten ohne Probleme ihren Dienst, obwohl es in den Kneipen schon etwas rauher zugeht.

Zunächst wird auf den Rechner die Linux-Distribution der Wahl installiert. Ich verwende Ubuntu, aber darauf kommt es nicht an. Es werden zwei Benutzer eingerichtet: ein administrativer („tresen“) und ein unpreviligierter („gast“). Achtung, bei Ubuntu ist der erste, bei der Installation eingerichtete User ein previligierter User (mit sudo-Rechten). Also erst „tresen“ und dann „gast“ einrichten und hinterher nochmal die /etc/sudoers überprüfen.

Man loggt sich zunächst als „gast“ ein und richtet den Browser so ein, wie man es gerne für die Gäste hätte: Adblocker, keine Cookies und Passwörter speichern, Startseite ect. Die Startseite für den Browser zeigt in unser Wiki, die Kneipengäste können dort dann eigene Links ablegen und so die Startseite etwas nach ihren Bedürfnissen anpassen. Der Webbrowser wird als Autostart-Applikation eingerichtet (System -> Preferences -> Sessions -> Startup Programs). Dann loggt man sich als „tresen“ ein und richtet den Anmeldungsmanager so ein, dass „gast“ beim Rechnerstart automatisch eingeloggt wird.

Jetzt kommt der interessante Teil: das Script pack_guest.sh verpackt das Home-Verzeichnis vom Gastuser in eine .tar-Datei:

#!/bin/sh
#
# Erstellt ein neues Template (gast_home.tar.gz) aus dem aktuellen gast
# Homeverzeichnis. Dieses Script sollte nur ausgeführt werden, wenn die
# Änderungem am gast-User dauerhaft gespeichert werden sollen.
#
# siehe http://wiki.freifunk-potsdam.de/index.php?title=Olga-Surfstation
if [ `whoami` != "root" ]; then
  echo "Du musst root sein, um das Script auszufuehren! Schau Dir bitte die"
  echo "Doku auf unserer Webseite an:"
  echo "http://wiki.freifunk-potsdam.de/index.php?title=Olga-Surfstation"
  exit 1
fi
 
#
DATE=`date +%Y%m%d_%H%M%S`
TARGET=/home/tresen/Surfstation/gast_home.tar.gz
 
# backup vom alten $HOME anlegen...
if [ -e $TARGET ]; then
  mv -v $TARGET  /home/tresen/Surfstation/gast_home_$DATE.tar.gz
fi
 
cd /
tar czf /home/tresen/Surfstation/gast_home.tar.gz /home/gast

Dieses Script ruft man einmalig auf und findet dann in /home/tresen/Surfstation die Datei gast_home.tar.gz. Dieses Template kann nun immer ausgepackt werden, wenn der Rechner startet. Das erledigt das Script unpack_guest.sh:

#!/bin/sh
#
# stellt den gast-user aus dem vorgefertigten Template (gast_home.tar.gz)
# wieder her. Das ist das Standard-Script, das bei jedem Booten ausgefuehrt
# wird. siehe
# http://wiki.freifunk-potsdam.de/index.php?title=Olga-Surfstation
#
if [ `whoami` != "root" ]; then
  echo "Du musst root sein, um das Script auszufuehren! Schau Dir bitte die"
  echo "Doku auf unserer Webseite an:"
  echo "http://wiki.freifunk-potsdam.de/index.php?title=Olga-Surfstation"
  exit 1
fi
 
rm -rf /home/gast
cd /
tar xzf  /home/tresen/Surfstation/gast_home.tar.gz
 
# falls das Passwort zurueck gesetzt wurde...
echo "gast:gast" | chpasswd

Jetzt muss man nur noch dafür sorgen, dass dieses Script bei jedem Bootvorgang ausgeführt wird. Eine Zeile in /etc/rc.local genügt:

/home/tresen/Surfstation/unpack_guest.sh

[Update] (12.11.09): Mir ist gerade aufgefallen, dass es mit Upstart zu einer Race-Condition kommt. Es ist einfach nicht definiert, wann das Script unpack_guest.sh ausgeführt wird und das führt zu sehr merkwürdigen Effekten.

Wenn man also eine Distribution mit Upstart verwendet (z.B. Ubuntu 9.10 „Karmic Koala“ oder später), muss man den Aufruf des unpack_guest.sh Scriptes im Upstart-System vor den gdm legen. Entweder man definiert einen eigenen Dienst oder man fügt das Script als pre-start Script in /etc/init/gdm.conf ein:

pre-start script
    # unpack the guest account home directory
    /home/tresen/Surfstation/unpack_guest.sh
end script

[/Update]

Mir gefällt an diese Lösung, dass wir damit absolut keine Arbeit haben. Die einzigen Probleme sind Hardwareschäden, also wenn wiedermal Bier über die Tastatur gekippt wurde. Das System an sich läuft aber nahezu wartungsfrei. Man muss sich halt alle paar Monate (idealerweise remote) einloggen und die fälligen Updates einspielen.