Monday, February 27, 2006

MS Natural Keyboard 4000: Scroll with Zoom Slider

For quite some time now I have been using the Microsoft Natural Keyboard 4000. While being generally satisfied with it, I have always disliked the software support for the Zoom Slider. You can modify the mappings of the special access keys above the F-Keys, but you cannot tell the mostly useless (at least for me) zoom slider to do something more sensible, e. g. scroll up and down. At least not with the IntelliType GUI.

Just a moment ago I searched the net again and found a solution! Have a look here for the somewhat lenghty discussion thread to read the whole story.

For the impatient I copied the vital information:

Therefore pick whatever application you wish to change the
functionality for, eg Internet Explorer (no I don't use it either).


<Application UniqueName="IEFrame" AppName="Internet Explorer">
  <C319 Type="6" Activator="ZoomOut" />
  <C320 Type="6" Activator="ZoomIn" />
</Application>


Changing this to:


<Application UniqueName="IEFrame" AppName="Internet Explorer">
  <C319 Type="6" Activator="ScrollUp" />
  <C320 Type="6" Activator="ScrollDown" />
</Application>


Means that the zoom slider now scrolls in ie.

Since I'm lazy I just replaced all the c319 and c320 lines with scroll,
this probably isn't wise as some of them are customised to do different
things depending on the application, but if your also lazy fire up vim
and use:

:%s/^.*<C319.*$/<C319 Type="6" Activator="ScrollUp" \/>/gc
:%s/^.*<C320.*$/<C320 Type="6" Activator="ScrollDown" \/>/gc

I hope the original poster does not mind me pasting this here.

Sunday, February 26, 2006

Star Wars: Empire at War

I have just installed the Demo of Star Wars: Empire at War. Being a Star Wars fan I was happy to see that they still sometimes make good games. Many of those wannabe Star Wars games just did not deserve the name. Either they had bad graphics, bad stories or even both. The last thing I really had time to play with and liked was X-Wing Alliance. Apart from that some Jedi Knight 2, and Voyager: Elite Force 2, both of which I enjoyed very much.

Being more the space combat type (I really loved the old TIE-Fighter, why don't they make such games any more?) I was pleased by how many options there are in "Empire at war". You are not limited to a Command-And-Conquer like ground combat all the time, but can also fight space battles. I played through the 4 tutorials available in the demo and loved it right from the beginning. The UI looks nice and clear and seems to be planned carefully as to not keep you too much from looking at the nice game graphics. The units seem to be quite clever, not just standing still while being shot at, just because I did not explicitly order them to defend themselves.

Because I already know I will not have the time to really play the game I will not buy it. However if I did, I would surely need a new graphics board... My old GeFore5600 is not up to the task, except with lowest detail settings, and I bet it will look way better with all the effects sliders being pushed farther to the right :)

Windows XP Compressed Folders buggy

For some time now several colleagues have been calling me for help, because they have problems installing a new version of the checkstyle plugin for Eclipse I provided them. Eclipse just won't recognize the plugin after they unpacked the ZIP file to the plugins folder. After some observation it turned out that only Windows XP users are having difficulties.

Wait a second, only Windows XP? No Linux, no Windows 2000 machines, just XP? What difference should there be in the Java world? (Well... I know... But at least the Windows machines should behave identically). At first I could not see anything special on those machines. Eclipse running fine, the checkstyle directory correctly placed under the plugins directory. After some time I noticed that there were far too little files in the plugin's subfolder.

Turns out that it is the Windows XP ZIP file integration into the explorer as "compressed folders". While the ZIP file has been created with WinZip (and extracted with it on the Windows 2000 machines), most of the XP users used the compressed folders function to put plugin into their Eclipse directories. And guess what: It just left out some files, and did not even tell them about it!. So at first glance everything is fine. The extraction completes, you can see the directories fine. But only a more thorough investigation reveals that only about half of the ZIP file's contents have really been extracted. Even better: Running the unpacking again on the same file produces a different set of results! Has anyone seen this behaviour? I cannot believe that they even manage to break a tool they initially bought from a third party company...

This makes me remember that we used to have problems with ZIP files being extracted on Windows 20003 servers, too. However there we did not miss any files, but some jars were corrupted after un-zipping the archive they were contained in. Unzipping the same file again usually fixed the problem, so that we even swapped memory, suspecting a faulty RAM module. For some other reason we switched to using a different (command line) unzip utility and have never seen a corrupted jar again.

Friday, February 24, 2006

Google Pages

Seems it was just fast enough to create my Google-Page before they closed it for new accounts due to overwhelmed servers. I do not have any particular use for it, however it was somewhat impressive to see and try out what can be done with some Javascript.

More detailed thoughts and criticism can be found on the page itself: daniel.schneller.googlepages.com

Thursday, February 23, 2006

toString() ought to make life easier, BUT!

I have always liked a sensible toString() implementation that allows me to quickly grasp the important aspects of an object when I just have a look at it in the debugger or print it to some logger.

A few weeks ago I read Joshua Bloch's "Effective Java" which also encourages you to provide meaningful implementions of toString() in your programs to make life easier for yourself and others using your stuff.

Today I was asked if I could help a colleague of mine with an error that had occured in a production environment (have a look at Russ Olsen's Weblog to learn about the debugging guy...). He showed me a stacktrace like the following:

java.lang.NullPointerException
  at some.package.TheClass.toString(TheClass.java:2264)
  at java.lang.String.valueOf(String.java:2131)
  at java.lang.StringBuffer.append(StringBuffer.java:370)
  at some.other.package.InterestingClass.relevantMethod(InterestingClass.java:292)
  at some.other.package.CallingClass.aMethod(CallingClass.java:229)
  ...

In the really InterestingClass we have a try-catch block that catches exceptions and wraps them in more general ones and re-throws them, like this:

try {
...
} catch (SpecialCustomException e) {
   throw new MoreGeneralCustomException(this.getClass(), this, "Field access error: "+fieldName+" in "+anObject, e);
} 

However the following little piece of code managed to leave us totally in the dark about what had really happened:

public String toString() {
  StringBuffer tStringBuffer = new StringBuffer(256);
  tStringBuffer.append("ValueClass:");
  tStringBuffer.append(getAnAttribute().getSomethingElse());
  return tStringBuffer.toString();
}

As you may have guessed by now, the line with the getAnAttribute().getSomethingElse() is the "at some.package.TheClass.toString(TheClass.java:2264)" in the stacktrace above. Clearly someone did not think about the possibility of getAnAttribute() returning null. Because this NPE now occured while constructing the detail message for our own custom exception, it did never come to call the MoreGeneralCustomException constructor, which in turn would have told us where to look for the real mistake in the program.

I have looked through our application sources and found around 200 customized toString's. I guess I will have to go over them one by one to make sure something like that is not still lurking around somewhere else...

Where has all the space gone...

Time and again I find my harddisk "becoming too small". Adding more and/or bigger disks doesn't really help, they just tend to fill up with all sorts of stuff faster than you can replace them.

Having not enough space left again to download the current Fedora DVD I remembered a little tool I once read about. Maybe it is already widely known about, however for those who haven't heard about it: I suggest you have a look at SequoiaView. It almost like some sort of profiler for applications: while in your code the parts that are really slow are hardly the ones you would expect to be, the things eating up lots of space on your hard drive, too, are seldom the ones you'd suspect.

Thursday, February 16, 2006

Tapeware has been replaced!

After all the trouble I went through with Yosemite Tapeware I finally abolished it. Looking around for some scripts around Windows' integrated backup (NTBackup) I found this (German) very good howto (English version here) by Denis Jedig (thanks for the great work, it saved me a couple of hours).

After I had collected the GUIDs for the devices, libraries and other items described it was just a matter of writing some wrapper scripts to manage service stops/starts before and after the backup. First full backup went through without any problems, sent me the expected report via mail and logged everything nicely.

I will now have a closer look at the log files of the incremental backups and behaviour when wrong or empty tapes are inserted as well as when they get full. I will keep you posted, in case anything interesting happens.

Monday, February 13, 2006

No more Tapeware

Tapeware corrupted its database again. Took three runs to repair it this time. And now (again) it does not recognize the drive.... Looking through their knowledge base I found comments about "inherently flawed database technology" that could be corrupted by a simple antivirus scan of the database directory. Although I have already taken care of everytning I could think of to keep the database healthy it keeps corrupting.

From what I learned the successor of Tapeware, Yosemite Backup, does not contain the bad database code anymore, however I still do not think that a product version 7 should be *that* broken. If anyone asks me, *don't* use TapeWare! God knows if you will be able to restore the data when you really need it.

I think I have had it with that damn thing. I will just try Microsofts windows backup software, maybe somewhat spiced up with a few batches. Hopefully that will be less unnerving...

Solved: Binary File corruption in Eclipse/CVS

No wonder I could not reproduce the problem with corrupted binary files in Eclipse at home: Turned out I had already updated my home PC, while at the office I had not. I have filed Bugzilla #127436 against Eclipse 3.1.0. Latest version 3.1.2 does not seem to have the problem, so I would advise anyone using Eclipse, CVS and branches to update to 3.1.2.

Here where I work we still have to see how we can achieve that on all workstations...

Saturday, February 11, 2006

Strange binary file corruption in CVS/Eclipse

We have experienced some weird binary file corruptions lately. Somehow Eclipse believed it had to replace every LF with a CRLF, which is not too healthy for .exe files.

First we thought we had just forgotten to set the -kb flag, but it turned out, that this could not be true for all of the broken files we saw. We were even able to reproduce the problem once, but now, as I am trying to write a bugzilla entry for Eclipse (3.1.2) I cannot get it *wrong* again.

From what I remember we checked in a new binary file on HEAD. Then we did a "Compare with another branch or version" on the branch and told Eclipse to "Override and Update". On Friday this made the .exe file grow from 15995 bytes to 16030 bytes; a binary compare showed exactly 35 LFs that happened to be in it being replaced with CRLFs on checkout. However now I cannot reproduce it anymore, so that I would not even trust my own bug report.

Seems I will have to wait till Monday and try to get hold of the problem together with the colleague that initally had the problem. Leaves a bad feeling to say the least...

Wednesday, February 08, 2006

Tapeware update hassles

A friend of mine uses Tapeware to backup his server to tapes. The software was bundled with the tape drive (Certance/Seagate 40GB IDE) and because it looked quite ok we went ahead and installed it. For some months now it has been running more or less smoothly, however once in a while it told me about a corrupted tape/file database (where it stores where which version of each file is placed on the tapes). If that happens, you have two choices: restore the last backup of the database from tape or take the backup service down and have it repaired via a somewhat lenghty process.

Three days ago it complained about a broken database again. I do not know what happens to corrupt it, because ususally the server just sits there, handing out files and database connections and backing up during the night when there are no clients. When I tried to restore the last backup of the database from tape it did not even let me open the restore menu (because of the corrupted database(!)), so I had to go through the repair process. This involves taking the service down, editing a config file, starting the client program on the server, wait, wait, wait, restart the service, and go over all the settings just to make sure.

While waiting on the repair process I surfed the net, looking for possible causes of the corruptions. I found a service pack release (7D) of TapeWare. The release notes contained something about resolved bugs that would cause database corruption, so I thought it would be worth a try and downloaded the installer.

It installed right away, no error messages or warnings at all, and when it was finished the About-dialog proudly presented the new version number. Everything seemed just fine, so I left with a feeling of success...

...until the next morning when I logged in from remote and had a look at the logfile. There it told me that the backup had failed, because "Tapeware could not get exclusive access to the device". Looking more closely I found a yellow exclamation mark on the streamer device's icon that definitely had not been there the night before. Every attempt I made to get it away was unsuccessful. I stopped and started the service, tried the database repair and finally even rebooted the machine, all to no avail. All entries in Yosemite-Tech's knowledge base just suggested to remove the streamer from the Windows device manager and have it re-detected, but I could not even see it there, just in the Tapeware GUIs devices section. Maybe that's be different for SCSI devices... However, in the end I decided to reinstall the damn thing. I made copies of the database subfolder and the Tapeware.Ini file (that contains the serial number) and started the uninstaller. Of course, to my great pleasure, this required another reboot... Finally I ran the service pack installer, without installing from the CD before. When it was finished (oh, no reboot here!) the streamer was working again. I just had to move back the saved database subfolder and my scheduled backup jobs reappeared, too. Now I will cross my fingers and wait until tomorrow to see if the backup job completes ok.

In the end I just feel my reservations against Windows as a server confirmed again. There are just too many things that you can neither understand, nor are there the appropriate tools to diagnose problems. But of course this is just what you have to cope with when you depend on specialized applications that are not available for Linux.

XML/XSL based changelog from ViewCVS

For some time now I have been working on generating useful changelogs from a ViewVC/ViewCVS MySQL database. After the hassles I talked about already I finally got it working quite fine. Because the people who are interested in the changelogs like GUIs, I wrote a little Swing application around the core functionality that I can't give away. But the real work is done in just a single SQL query and some XML/XSLT processing.

Basically there are three steps to be done:

  1. Query ViewCVS database
  2. Generate XML based on the result
  3. Transform with XSLT to something readable

I use a query like the following, because we usually do changelogs per branch and for a certain period of time. There is no easy way to query tags, because they are not part of the database, but we found it quite convenient to just keep a list of tags and their associated timestamps. To query the head, look for branch=''.

select c.type, c.ci_when, p.who, b.branch, 
       SUBSTRING_INDEX(d.dir, '/', 1) as project, 
       cast(concat_ws('/', d.dir,f.file) as CHAR) as file, 
       c.revision, descs.description
from checkins c 
  join branches b on c.branchid=b.id
  join files f on f.id=c.fileid
  join dirs d on d.id=c.dirid
  join repositories r on r.id=c.repositoryid
  join descs on descs.id=c.descid
  join people p on p.id=c.whoid
where b.branch='aBranchname'
  and ci.when between 'timestamp1' and 'timestamp2'
order by project, description, dir, file, revision

If you like to query by checkin comment, you might consider placing a fulltext index on the description column in the descs table.

The result from that can now be translated into XML. The schema is similar to that of the cvs2cl.pl script, but I adapted it a little to better suit our needs, e. g. to allow grouping by a checkin ticket number. The following code fragment performs the generation of an XML document. It needs to be "padded" somewhat to be runnable.

I have attached a file changelog_generator.pseudocode.java that contains the following code and some more utilities, including the XSL stylesheet. Unfortunately I cannot upload an archive containing the original files.

/**
 * Create a Map based on the ResultSet containing the rows from the ViewCVS query.
 * @param aResultSet the ResultSet 
 * @return a Map, having the detected ticket numbers as keys and Lists of CvsCommitEntrys as values.
 * @throws SQLException if something unexpected happens on the JDBC layer
 */
private Map generateEntryMap(ResultSet aResultSet) throws SQLException {
 Map entryMap = new HashMap();
 String ticketNumber;
 Pattern tPattern = Pattern.compile("(WK|RWK|RDE|RIT|RQ|REQ)\\d{1,8}");
 while (aResultSet.next()) {
  CvsCommitEntry tEntry = new CvsCommitEntry();
  tEntry.setDescription(aResultSet.getString("description"));
  tEntry.setFile(aResultSet.getString("file"));
  tEntry.setProject(aResultSet.getString("project"));
  tEntry.setTime(aResultSet.getTimestamp("ci_when"));
  tEntry.setAuthor(aResultSet.getString("who"));
  tEntry.setRevision(aResultSet.getString("revision"));
  tEntry.setBranch(aResultSet.getString("branch"));
   String tType = aResultSet.getString("type");
  if ("Remove".equals(tType)) {
   tEntry.setState("Dead");
  } else {
   tEntry.setState("Exp");
  }
   Matcher tMatcher = tPattern.matcher(tEntry.getDescription());
  if (tMatcher.find()) {
   ticketNumber = tMatcher.group(0);
  } else {
   // this text if no ticket number was found
   ticketNumber = "ohne Fehlernummer";
  }
  tEntry.setErrorNumber(ticketNumber);
  if (entryMap.containsKey(ticketNumber)) {
   List tList = (List)entryMap.get(ticketNumber);
   tList.add(tEntry);
  } else {
   List tList = new Vector();
   tList.add(tEntry);
   entryMap.put(ticketNumber, tList);
  }
 }
 return entryMap;
}
/**
 * Creates an XML file or console output based on a Map of CvsCommitEntrys.
 * @param someEntries the Map of entries to process
 */
private void generateOutput(Map someEntries)  {
 Map tParamMap = new HashMap();
 
 /* where to put the resulting file */
 String tOutputDir = "/tmp/outputdir";
 ByteArrayOutputStream tByteArrayOutputStream = new ByteArrayOutputStream();
 PrintWriter outwriter = new PrintWriter(new OutputStreamWriter(tByteArrayOutputStream));
 DateFormat tDateOnly = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
 DateFormat tTimeOnly = new SimpleDateFormat("HH:mm", Locale.ENGLISH);
 DateFormat tDateTime = new SimpleDateFormat("yyyy-MM-dd, HH:mm", Locale.ENGLISH);
 DateFormat tWeekdayFormat = new SimpleDateFormat("EEEE", Locale.ENGLISH);

 outwriter.println("");
 outwriter.println("");
 
 /* the following are just for the headline of the changelog */
 tParamMap.put("starttag", "tagname");
 tParamMap.put("endtag", "anothertagname");
 tParamMap.put("branch", "branchname");
 outwriter.println(startTag("changelog", tParamMap));
 tParamMap.clear();
 Object[] tKeys = someEntries.keySet().toArray();
 Arrays.sort(tKeys);
 for (int i = 0; i < tKeys.length; i++) {
  String tKey = (String)tKeys[i];
  List tEntries = (List)someEntries.get(tKey);
  CvsCommitEntry tEntry = (CvsCommitEntry)tEntries.get(0); // get the first for some common data
  outwriter.println(startTag("entry"));
  outwriter.println(startTag("errorcode") + tEntry.getErrorNumber() + endTag("errorcode"));
  outwriter.println(startTag("date") + tDateOnly.format(tEntry.getTime()) + endTag("date"));
  outwriter.println(startTag("weekday") + tWeekdayFormat.format(tEntry.getTime()) + endTag("weekday"));
  outwriter.println(startTag("author") + tEntry.getAuthor() + endTag("author"));
  for (Iterator tIterator = tEntries.iterator(); tIterator.hasNext();) {
  CvsCommitEntry tCurrentEntry = (CvsCommitEntry)tIterator.next();
   outwriter.println(startTag("file"));
   if (tCurrentEntry.getErrorNumber().equals("ohne Fehlernummer")) {
    // no ticket number was found, add the author to each entry 
    outwriter.println(startTag("author") + tCurrentEntry.getAuthor() + endTag("author"));
   }
   outwriter.println(startTag("name") + replaceXmlSpecialChars(tCurrentEntry.getFile()) + endTag("name"));
   outwriter.println(startTag("cvsstate") + tCurrentEntry.getState() + endTag("cvsstate"));
   outwriter.println(startTag("revision") + tCurrentEntry.getRevision() + endTag("revision"));
   outwriter.println(startTag("branch") + tCurrentEntry.getBranch() + endTag("branch"));
   outwriter.println(startTag("tag") + endTag("tag"));
   if (tCurrentEntry.getErrorNumber().equals("ohne Fehlernummer")) {
    outwriter.println(startTag("time") + tDateTime.format(tCurrentEntry.getTime()) + endTag("time"));
   } else {
    outwriter.println(startTag("time") + tTimeOnly.format(tCurrentEntry.getTime()) + endTag("time"));
   }
   outwriter.println(startTag("msg") + replaceXmlSpecialChars(tCurrentEntry.getDescription())
     + endTag("msg"));
   outwriter.println(endTag("file"));
  }
  outwriter.println(endTag("entry"));

 }
 outwriter.println(endTag("changelog"));
 outwriter.flush();

}

The resulting XML can be transformed with the included XSL template. For more information on the grouping based on the "Muenchian Method" used in the stylesheet refer to this very good description. I learned how to do it from there, too.

If you put XML and XSL into the same directory, opening the XML with a browser will usually cause it to render the output according to the stylesheet automatically. Of course you can also use Xalan or some other processor to render it statically.

Tuesday, February 07, 2006

PuTTY command line

PuTTY is my favorite SSH Client. However its configuration is somewhat cumbersome. Using a little batch file allows me to apply a set of default settings to any host I like.

Where I work we have a lot of Linux based clients around the world that function mostly without any manual intervention on behalf of an administrator. However there are times of course, when we still need to access one of those systems. Unfortunately there are literally hundreds of them, and anybody who has used PuTTY before knows that it is not fun to create a session entry for each and every one, including port forwarding options, private key file etc. Of course, one might say you could just export the Registry key for a single session and use that as a template and reimport it into the registry. However this is a single-shot approach, because after the copy has been made, you cannot globally change a setting, say the path of the private key file you want to use. Furthermore you may not be allowed to edit the registry on any given system.

While looking for a solution and examing the PuTTY command line options I found that you can easily mix all command line options (e. g. tunnel options, key file name etc.) with a "-load" option. This allows you to define a "template" session via the PuTTY GUI that includes everything you need to configure but cannot specify on the command line (e. g. the character translation). So in the end I came up with a simple Windows batch file that has the follwing content:

echo Hostname:
set /p hst=
putty.exe -C -2 -4 -i ".\keyfile.ppk" -L 5999:127.0.0.1:5901 -l username -ssh -load "Template Session" %hst%

This lets me first enter the hostname or IP address of the host I want to connect to and then opens a session that is based on the configured settings in "Template Session" and completed with the options -C (compression), -2 (SSH2), -4 (IPv4), -i (which keyfile to use), -L (tunnel specification), -l (the remote username), -ssh (SSH protocol) and finally the hostname I just entered.

This has made my life a lot easier, because with this I can finally connect to any box I like while still having the flexibility of a global set of preferences that can be changed easily. Of course, some of the options are not strictly neccessary (like the -4 option), but they do no harm either.