Ever wonder how how modules actually work in FVWM? I did. So I decided to learn about them, by writing one of my own. And since I’m learning, I thought I’d share the experience.
My intention is to write a module called FvwmSchedule to do schedule commands for subsequent execution. As a warm up though, and to get the hang of this module writing lark, I’m going to start with a more modest goal: FvwmNull does absolutely nothing, although it will do so in the manner proper for FVWM modules. Once we have our null module working, it will be a lot easier to add functionality to create our scheduler.
My implementation language is going to be C, and I’m going to assume a basic familiarity with that language. If you are planning a module in Perl/Bash/Python/Whatever you can probably still make use of a lot of this information.
Ready? Then let’s go. First of all let’s create a working directory:
cd ~/.fvwm
mkdir mods
cd mods
Then this line to the end of your config to include it in your ModulePath:
ModulePath $[HOME]/.fvwm/mods:+
You’ll also have to restart FVWM or else paste the command into a console. Once this is done we’re ready to start coding.
Our first port of call in writing our module is the Fvwm module documentation. This tells us almost everything we need to know.
To begin with it tells us the arguments our module should expect. Basically, arg zero is the file name, as always, and arg 1 and 2 are the file descriptors used to respectively write to and read from FVWM. There are another three args which we can safely ignore, and any after that are command line arguments from the module invocation
So our first cut at the module looks like this:
#include <stdio.h>
static char *mod_name = NULL;
main(int ac, char *av[], char **envp)
{
int i;
char *tmp;
/*
* OK the first (subscript zero) argument is the full module path
*/
mod_name = av[0];
/*
* we'll need to chop the path off the name
*/
if ((tmp = strrchr (mod_name, '/')) != NULL) {
mod_name = tmp + 1;
}
fprintf (stderr, "module name: %s\n", mod_name);
/*
* check args - should be at least 6
*/
if (ac < 5) {
fprintf (stderr, "%s: must be started by FVWM\n");
exit (1);
}
/*
* we're only really interested in the first three args - the module
* name and the two file descriptors - but let's print this stuff anyway
*/
for (i = 0; i < ac; i++) {
fprintf (stderr, "arg %d: <%s>\n", i, av[i]);
}
return 0;
}
Create a file called FvwmNull.c containing the code above in the mods subdirectory you created in ~/.fvwm. Then compile it by typing
Make FvwmNull
Congratulations! You just created a working module! You can start it in from a console window like this:
Module FvwmModule
Output from the module goes to standard out. That means it’ll probably end up in .xsession-errors, or on the screen of the virtual console that launched FVWM. I redirect stdout and stderr to a log file
FVWM_DIR=$HOME/.fvwm/
exec /usr/bin/fvwm -f $FVWM_DIR/.fvwm2rc > $FVWM_DIR/log 2>&1
Anyway, however you do it, take a look at the output from FVWM and you’ll see the output from your module. It should look a bit like this:
module name: FvwmNull
arg 0: </home/nick/.fvwm/mods/FvwmNull>
arg 1: <23>
arg 2: <4>
arg 3: <none>
arg 4: <0>
arg 5: <8>
And that’s it. The module finishes its initialisation and then terminates. That’s somewhat useless even by the standards of FvwmNull. We need a main loop. According to the web site, we should be able to read on the file descriptor passed in av[2] and we’ll get stuff (to use the technical term) from FVWM. Add some variables:
char buff[1024];
int fd_write, fd_read;
and then at the end of main:
fd_write = atoi(av[1]);
fd_read = atoi(av[2]);
for(;;) {
int rc = read(fd_read, buff, sizeof(buff));
fprintf(stderr, "read: %d bytes\n", rc);
}
Compile it and run it. In theory, if we send a command using SendToModule from the console, the read statement should pick it up. We’re not going to see more than the number of bytes read but we should see something.
Try this:
SendToModule FvwmNull Hello Fvwm!
Did you try it? Did it work? Nope, me neither
I found the problem by digging through source code for a few modules, FvwmDebug in particular. Basically you have to send the command “NOP FINISHED STARTUP” to FVWM before it will start talking to your module. This isn’t mentioned anywhere on the module page, and is barely eluded to in the fvwm man page. Nevertheless, now we know the secret we can master this process for ourselves. Assuming we can figure out how to figure out how to send messages back to FVWM that is.
Back to the module page on the website. There’s a function called SendText which outlines the code we need. I fiddled with it a bit, but this is the same basic function.
void send_to_fvwm(int fd, char *message)
{
int len, tmp;
long window = 0; /* no window context */
/*
* no message? no problem!
*/
if (message == NULL) {
return;
}
/*
* send the window context. there isn't one in this case,
* and so we send zero
*/
write (fd, &window, sizeof(window));
/*
* get the length of the message and send it to FVWM
*
* we really should check the return codes from these...
* what should we do on an error though?
* I'll come back to that...
*/
len = strlen(message);
write (fd, &len, sizeof (len));
/*
* now send the message
*/
write (fd, message, len);
/*
* send 1, indicating that this module will keep going
* 0 would mean that this module is done
*/
tmp = 1;
write (fd, &tmp, sizeof(tmp));
}
Add that in and then call it just before the loop. An important note here: don’t try and stop the module with KillModule just yet. We don’t have the code in place to deal with that, and all you will do is send your module into a busy loop and fill up your logfile with “read: 0 bytes” lines. Instead use
killall FvwmNull
That said, recompile and launch the module again. the log file should show something like
module name: FvwmArt
arg 0: </home/nick/.fvwm/mods/FvwmArt>
arg 1: <23>
arg 2: <4>
arg 3: <none>
arg 4: <0>
arg 5: <8>
read: 36 bytes
read: 80 bytes
read: 36 bytes
read: 80 bytes
read: 36 bytes
read: 80 bytes
read: 36 bytes
read: 80 bytes
And all that before you even try sending anything! What’s going on?
Well, what happens is that FVWM sends notification of all events to all modules by default. What you see in all those different read lengths are things like raise-window, lower window - anything that FvwmEvent might pick up. Which is how EvemEvent works, of course.
Still, we’re not interested in all of those events. For some modules we’d need to track this stuff, but for our present purposes we can mask this stuff off. The FVWM module page describes how to mask events. It says you send the bitwise OR of all the event types you want to receive. The following example is provided.
SetMessageMask(fvwm_fd, M_STRING | M_CONFIG_INFO | M_SENDCONFIG | ...);
SetMessageMask(fvwm_fd, MX_VISIBLE_ICON_NAME);
The function SetMessageMask is defined in Modules.c in the libs directory of the FVWM standard distribution. I’m not going to use the FVWM library in this article however. Partly that’s because I don’t have the library installed as part of my FVWM install, and partly because you learn more working at low-level anyway. That said, if you want to build the library and compile and link against it, you can save a bit of time over they way I’m doing it. End of digression.
Looking at the SetMessageMask function, all it does is send a string to FVWM consisting of “SET_MASK” followed by the mask value. We only want to get SendToCommand strings. These are M_STRING commands, which are defined on the module page as
#define M_STRING (1 << 22)
So if we add that definition to the top of the module source, we can then set the event mask, just before the “NOP FINISH STARTUP” send:
sprintf(buff, "SET_MASK %lu", M_STRING);
fprintf(stderr, "%s: setting mask: <%s>\n", mod_name, buff);
send_to_fvwm(fd_write, buff);
Compile it and start it up. your log shouldn’t show any mode events from focus changes or window raising. In fact you won’t see any events unless you issue a SendToModule command from a console.
SendToModule FvwmNull foo
Play around with that for a bit. You’ll find from the read sizes reported that there are 32 bytes worth of overhead for each message and that all read sizes are in multiples of four. We’ll investigate the structure of these messages in just a moment.
Before we do that, let’s fix that infinite loop on KillModule bug. This turns out to be quite simple. All fvwm does is close the pipe from its end. To us, that looks like an end-of-file condition, with zero bytes being returned. So after the read add
if(rc == 0) {
fprintf(stderr, "pipe closed - exiting\n");
break;
}
And that will take care of that problem. Strictly we need to catch SIGPIPE as well, but we don’t really need to worry about that in FvwmNull, so I’ll defer that discussion until later on.
Now then - about those packets from FVWM. According to the developer pages on the FVWM website the message header is an array of four four byte long integers.
The first long should always hold the value 0xffffffff. If not it means we had a partial read somewhere. If we wanted to we could read scan forward for the next 0xffffffff byte and use that to re-synchronise with FVWM, but I’m not going to worry about that unless I have to.
The second long describes the packet type using the constants listed on the module developer web page. We only expect M_STRING packets, since that’s the mask we set. You can get the authoritative list from libs/Module.h in your FVWM source directory.
The third long is the total length of the packet in unsigned longs, including the header we just read. So to get the remaining bytes to be read, we subtract 4 and multiply the result by 4.
The last long in the header is an X-Windows time stamp in microseconds. I expect that could be useful for double-click type events and so forth. We can safely ignore it though.
Let’s have a struct to hold those values:
typedef struct fvwm_head {
long sync;
long pkt_type;
long pkt_length;
long timestamp;
} FVWM_HEAD;
Then we can rewrite our main loop like this:
for(;;) {
int rc, len;
FVWM_HEAD header;
/*
* normally we'd use select() for this, but since we're
* quite happy to hang around waiting for FVWM,
* we can use read() instead
*/
rc = read(fd_read, &header, sizeof(header));
if(rc < 0) {
fprintf(stderr, "error on read\n");
break;
}
if(rc == 0) {
fprintf(stderr, "pipe closed - exiting\n");
break;
}
/*
* check for proper synchronisation, then
* get the length of the remainder of the packet
*/
assert(header.sync == 0xffffffff);
len = (header.pkt_length - 4) * 4;
/*
* now, what we do next depends on the packet type
*/
switch(header.pkt_type) {
case M_STRING:
read_m_string(fd_read, len);
break;
default:
/*
* working on the principle that a crash causing bug
* gets fixed fastest, I'll make this one fatal >:>
*/
fprintf(stderr, "%s: fatal error: unexpected packet type
%lu",
mod_name, header.pkt_type
);
exit(1);
}
}
There’s quite a lot there, but it’s just what I’ve described above. I’ve hived off reading the remainder of the packet into a new function called read_m_string. To find out how to write this we need to go back to that web page again and read the format for M_STRING packets. These turn out to be three window ids of 4 bytes each, in case the module was called in a window context, and also in case we cared. The rest of the packet is, finally, our string.
So all we need to do is allocate some memory, read the message, ignore the first 12 bytes, print the string to the log (so we know we got it right) and free the memory!
Something like this:
typedef struct fvwm_m_string {
long appl_win_id;
long frame_win_id;
long fvwm_internal_id;
char message[1]; /* over allocate to message length */
} FVWM_M_STRING;
void read_m_string(int fd, int len)
{
FVWM_M_STRING *spt;
/*
* allocate some memory - calloc sets it to binary zeroes. Which is nice.
* add one byte for the message null terminator
*/
if((spt = calloc(len+1, 1)) == NULL) {
fprintf(stderr, "%s: out of memory!\n", mod_name);
exit(1);
}
/*
* read the message - don't use sizeof(*spt) or sizeof(FVWM_M_STRING)
* since that'll only read one byte of the actual message
*/
if(read(fd, spt, len) <= 0) {
return; /* sort it out in the main loop :) */
}
fprintf(stderr, "%s: message received: '%s'\n",
mod_name, spt->message
);
free(spt);
}
And there it is - a fully functional, albeit functionally trivial, FVWM module. Next post I’ll use this framework to write the FvwmScheduler module I described at the start.
[edit] I missed out the error checking on the read in the main loop. Fixed now, but should still print errno for debugging.
[color=red]Edited by theBlackDragon:
–> Split the Perl part out and moved from General Fvwm discussion[/color]