Feb 282015
 

It just took me quite a while to find this information so I’ll put it here for future reference.

A Firemonkey application can not just access the clipboard, it needs to ask the platform whether it actually has one, then get the service interface and use that.

uses
  Fmx.Platform;

[...]
function TryGetClipboardService(out _clp: IFMXClipboardService): boolean;
begin
  Result := TPlatformServices.Current.SupportsPlatformService(IFMXClipboardService);
  if Result then
    _clp := IFMXClipboardService(TPlatformServices.Current.GetPlatformService(IFMXClipboardService));
end;

procedure StringToClipboard(const _s: string);
var
  clp: IFMXClipboardService;
begin
  if TryGetClipboardService(clp) then
    clp.SetClipboard(_s);
end;

procedure StringFromClipboard(out _s: string);
var
  clp: IFMXClipboardService;
  Value: TValue;
  s: string;
begin
  if TryGetClipboardService(clp) then begin
    Value := clp.GetClipboard;
    if not Value.TryAsType(_s) then
      _s := '';
  end;
end;

(This is for Delphi XE7.)

TEdit and TMemo have got a CopyToClipboard and PasteFromClipboard method. (Which begs the question: Why not implement a generic function for reading and writing a string rather than methods for two controls? I was tempted to use the Visual Basic solution: Put a hidden control on the form and use its methods. Bad memories awake …)

Updated dzEditorLineEndsFix

 Delphi  Kommentare deaktiviert
Feb 282015
 

I have just updated dzEditorLineEndsFix to address a small problem: The tool can be too fast so the file is already gone when Delphi tries to access it. It now waits 200 ms after detecting the file creation before moving it. This should solve the issue.

It’s available for download from the dzEditorLineEndsFix page on SourceForge.

On expiring Dirvish images

 Linux  Kommentare deaktiviert
Feb 162015
 

Dirvish is a backup solution for Linux (and probably other unixoid OSes). I use it to make a daily backup of one server to a different server located in a different building (it’s not the only backup solution we use but the most convenient one because we can access these files easily). Once set up, it runs automatically and I have configured it to send me an email with the result of the backup and the remaining free hard disk space. I’m not the only one who does that.

The backup server has multiple 2 tb disks mounted as a single btrfs volume, so the resulting disk space is huge. The backup has run flawlessly for over a year until now it started running out of space. So now is the first time I have actually to think about expiring old backups. (Bad Thomas, you should have given that a little bit more thought from the beginning.)

The way Dirvish handles expiry is like this:

You specify a default expiry rule and optionally more detailed expiry rules in either /etc/dirvish/master.conf or in the vault’s default.conf file. If you don’t change anything, most Dirvish installations will set the default to +1 year:

expire-default: +1 year

All this will do is add an Expire entry to the summary file of each image. To actually expire anything you must call dirvish-expire, but that call is usually added to cron automatically by the dirvish package via the /etc/dirvish/dirvish-cron shell script:

# other stuff ...
/usr/sbin/dirvish-expire --quiet && /usr/sbin/dirvish-runall --quiet

Now, expiring every image after one year is probably not the best backup strategy. You usually want to keep one copy every week, every month and every year for a longer period, maybe even forever. So more complex rules are required and must be added to either /etc/dirvish/master.conf or default.conf of an individual vault. I opted for keeping Friday backups forever and deleting everything else after 9 months, so the configuration looks like this:

expire-default: +9 months

# keep Friday backups forever
# (for everything else we use the default from above)
expire-rule:
#       MIN    HR      DOM     MON     DOW     STRFTIME_FMT
        *       *       *       *       fri     never

The rules follow crontab format, so the line above means:

  • ignore the minute
  • ignore the hour
  • ignore the day of the month
  • ignore the month
  • only for Friday

Here is a good explanation about how these rules work.

Now, since all this does is adding an entry to the summary file of each image, I have a problem: This takes care of all future backups, but all existing images were created with an expire-default of +1 year, so they contain corresponding entries like this:

Image: 2014-02-21_12-00
Reference: 2014-02-20_12-00
Image-now: 2014-02-21 12:00:02
Expire: +1 year == 2015-02-21 12:00:02

So the image was taken on 21 FEB 2014 and will be expired on 21 FEB 2015. That is a Friday backup, so I want it to be kept forever. Other images also have an expire entry of +1 year but I have to free up some disk space and therefore want to expire them after 9 months already.

What this means is that I need to change the expire entry in the summary files. All two hundred and something of them. That’s not something you want to do by hand because it’s boring and error prone.

I could probably write a shell script (but since I rarely do that it would also be quite error prone) or a Perl script (same problem even though I have got more practice with that). So I’ll write a Delphi program and access the files via a Samba share.

Jan 032015
 

Sometimes you just want to know how a program gets called by another program or by Windows. In that case this little batch file might come in handy:

@echo off
rem This batch file shows its full filename and its parameters
echo cmd file: %~dpnx0
echo Parameters:
for %%I IN (%*) DO ECHO %%I
pause

Blatantly copyied from This StackOverflow answer.

Dez 222014
 

Note to self:

If MS Access adds double quotes to field names in queries, do not use them!
It won’t complain about them (You’d wish it would), but it just won’t work.

So if you see something like:

SELECT *
FROM t_Mst_Tageserfassung
WHERE ("TeMitarbeiter"=92)  AND ("TeArbeitstag"=#12/24/2014#);

Remove the quotes and it will start to work as expected.

If you need to ensure that the fields are treated as field names, put them into parentheses like this:

SELECT *
FROM t_Mst_Tageserfassung
WHERE ((TeMitarbeiter)=92)  AND ((TeArbeitstag)=#12/24/2014#);

This also works with TAdoQuery in Delphi.

Nov 102014
 

By default, if no translation for a language is available, dxgettext will not do any translation but use the strings as they are in the source code. Sometimes this is not desirable. e.g.

  • Your customer does not understand the source language (e.g. your source language is not English but say German)
  • You are using dxgettext to convert special characters from a placeholder (e.g. “(R)” or “[deg]”) to the actual character (“®” or “°”)

In these cases you’d probably want the translation to default to a language that is actually supplied.

dxgettext doesn’t seem to have this feature (I looked quite hard) so I implemented it myself.

unit u_dzTranslator;

interface

// ... other stuff ...

///<summary>
/// Sets the language to use </summary>
procedure UseLanguage(_LanguageCode: string);

///<summary>
/// gets a list of languages for which translations are available </summary>
procedure GetListOfLanguages(const _Domain: string; _Codes: TStrings;
  _Languages: TStrings = nil);

///<summary>
/// Sets the language to use if the desired language is not available,
/// defaults to English </summary>
procedure SetDefaultLanguage(const _LanguageCode: string);

// ... other stuff ...

implementation

uses
  gnugettext;

// ... other stuff ...

const
  DEFAULT_LANGUAGE = 'en';
var
  gblDefaultLanguage: string = DEFAULT_LANGUAGE;

procedure UseLanguage(_LanguageCode: string);
var
  Codes: TStringList;
  CurLang: string;
  i: Integer;
  p: Integer;
begin
  gnugettext.UseLanguage(_LanguageCode);

  CurLang := gnugettext.GetCurrentLanguage;
  Codes := TStringList.Create;
  try
    GetListOfLanguages('default', Codes);
    for i := 0 to Codes.Count - 1 do begin
      if SameText(CurLang, Codes[i]) then begin
        // There is a translation for this language and country, everything is fine
        Exit; //-->
      end;
    end;
    // no translation found, try without the country code
    p := Pos('_', CurLang);
    if p <> 0 then begin
      CurLang := Copy(CurLang, 1, p - 1);
      for i := 0 to Codes.Count - 1 do begin
        if SameText(CurLang, Codes[i]) then begin
          // There is a translation for this language but not country, we can live with that
          Exit; //-->
        end;
      end;
    end;
  finally
    FreeAndNil(Codes);
  end;

  // we found no translation for this language, so we use the default language
  gnugettext.UseLanguage(gblDefaultLanguage);
end;

procedure SetDefaultLanguage(const _LanguageCode: string);
begin
  if _LanguageCode = '' then
    gblDefaultLanguage := DEFAULT_LANGUAGE
  else
    gblDefaultLanguage := _LanguageCode;
  UseLanguage(gnugettext.GetCurrentLanguage);
end;

procedure GetListOfLanguages(const _Domain: string; _Codes: TStrings; _Languages: TStrings = nil);
var
  i: Integer;
begin
  _Codes.Clear;
  gnugettext.DefaultInstance.GetListOfLanguages(_Domain, _Codes);
  if Assigned(_Languages) then begin
    _Languages.Clear;
    for i := 0 to _Codes.Count - 1 do begin
      _Languages.Add(languagecodes.getlanguagename(_Codes[i]));
    end;
  end;
end;

// ... other stuff ...

initialization
  SetDefaultLanguage(DEFAULT_LANGUAGE);
end.

Apart from the obvious, that is, setting a unit global variable to the desired default language, which itself defaults to English, this code changes the way UseLanguageWorks. It now does:

  • Call gnugettext.UseLanguage to let gnugettext do its stuff
  • Call gnugettext.GetCurrentLanguage to get the language that gnugettext uses (just in case gnugettext changes it from what was set with UseLanguage).
  • Gets a list of all supported translations
  • Tries to find a matching translation for the desired language and country.
  • If not found, tries to find a matching translation for the desired language, ignoring the country
  • If not found, changes the language to the default language.

Note that I just wrote this code, it might still contain bugs and is probably far from perfect. I will put it into the unit u_dzTranslator of my dzlib library and will fix any bugs I find in the future there.

Nov 052014
 

Today I spent several hours hunting down a problem with opening a COM port. Basically the program opens a COM port, writes some bytes to it, reads an answer and closes it again. This determines whether the expected device is connected to the COM port or not.

If the answer is the expected one, so the device is connected and active, the detection loop exists (the COM port has been closed using CloseHandle).

Now the actual communication with the device starts. The first thing is does, is open the same COM port again. Which fails with the error code 5 (ERROR_ACCESS_DENIED). WTF?

Of course, I tried to debug the issue using the Delphi integrated debugger. While I stepped through the code, the error disappeared, only to come back when I just run the code without stepping through it. This was reproducible (I love reproducible errors.).

When something like this happens, you can be sure it is a timing issue. Either your program is multithreaded and the issue is one thread doing something while the other is doing something else that conflicts with the first thread (called race condition or deadlock, depending on the outcome). Or it’s not your own threads but somebody else’s.

In my case this StackOverflow question seems to be about the same issue and the accepted answer was mentioning the FTDI drivers I am using (the COM port is actually a USB serial adapter). These drivers apparently do not immediately close the port when the handle is closed so opening it again can fail. It turned out that a retry loop when opening the COM port solved the issue. Here is the code I ended up using:

procedure TCustomComPort.CreateHandle;
const
  MAX_TRIES = 10;
var
  Tries: integer;
  OK: boolean;
begin
  Tries := 0;
  repeat
    Inc(Tries);
    FHandle := CreateFile(
      PChar('\\.\' + FPort),
      GENERIC_READ or GENERIC_WRITE,
      0,
      nil,
      OPEN_EXISTING,
      FILE_FLAG_OVERLAPPED,
      0);
    OK := (FHandle <> INVALID_HANDLE_VALUE);
    if not OK then
      Sleep(10 * Tries); // it wasn't enough to Sleep(10) here
  until OK or (Tries >= MAX_TRIES);

  if not OK then
    CallException(CError_OpenFailed, GetLastError);
end;

Some of you might recognize part of this code from the ComPort library for Delphi and C++. (But probably not, because the original code is just three lines. ;-) )

Of course the real debugging was more complex than I described above because the program is multi threaded and the detection is done in one thread while the actual communication is done with the foreground thread writing to the port and the answers being processed by a background thread. So of course, at first I suspected an error in my code, actually found one, fixed it, just to run into the next problem. Finally it turned out to be the driver problem described here.

Nov 042014
 

Once in a while I run into this problem and every single time it takes me forever to remember the cause:

Say, you have got an interface and a class implementing that interface:

type
  IMyInterface = interface
    function asMyInterface: IMyInterface;
  end;

type
  TMyClass = class(TInterfacedObject, IMyInterface)
  private
    function asMyInterface: IMyInterface;
  end;

[...]

function TMyClass.asMyInterface: IMyInterface;
begin
  Result := Self as IMyInterface; // compile error here
end;

This will fail to compile in the marked line with the error “Operator not applicable to this operand type”.

The reason is simple: In order for the as operator to work on interfaces, the interface must have a GUID assigned to it. You do that in the Delphi IDE by positioning the cursor after the interface keyword and press Shift+Ctrl+G. As soon as your interface declaration looks like this …

type
  IMyInterface = interface ['{93903D10-58F7-41B0-AFB1-2A8E17F828EF}']
    function asMyInterface: IMyInterface;
  end;

… the code will compile.

(Don’t just copy this code, you will need to generate your own unique GUID as described above! Otherwise you will experience strange things.)

Okt 292014
 

The current Avira Antivirus has got a module it calls “Internet scurity” which in turn has an option for e-mail security.

What it does is capturing all connections to POP3, IMAP and SMTP servers and scanning them for viruses. That sounds good until you find, that your e-mail client stopped working even though you haven’t changed anything in it’s configuration.

Why is that? It’s because of this e-mail “security” feature. If you are using encrypted transport (START TLS or SSL/TLS), Avira cannot read the traffic and blocks it! This is so stupid, there must be an award for it somewhere.

To work around this, you can of course

  • disable the e-mail security feature
  • disable transport encryption

The first has the drawback that Avira now complains that your computer is not secure. The second has the drawback that your e-mails can be read by everybody who can access your connection to the server. So both options aren’t really solutions. If Avira forces you to do that (e.g. if the following doesn’t work for you because your mail server does not have alternative ports for encrypted connections), your computer is actually less secure than without Avira e-mail “security”.

Then there is the third option: Don’t use the default SMTP (25), POP3 (110) and IMAP (143) ports but use different ones, e.g. the ones that are reserved for encrypted transports:

  • SMTPS: 465
  • POP3S: 995
  • IMAPS: 993

Unfortunately that means your mail servers must support these protocols / ports. E.g. if you are using postfix you have to change / uncomment the following lines in the /etc/postfix/master.cf file:

smtps     inet  n       -       -       -       -       smtpd
#  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes

And, or course, restart postfix.

The same applies to your POP3 or IMAP server.

If you can’t do that and your e-mail provider doesn’t support this, tough luck!

In addition, you must change the configuration in your e-mail client. In Thunderbird that’s pretty much straight forward:

WARNING: I will not fix your computer if you break it even if you followed my instructions. It’s your own responsibility. Making a mistake in the Thunderbird configuration will prevent it from receiving and/or sending e-mails.

  • Open the account settings dialog.
  • Select “Server Settings”.
  • Under Connection Security, select SSL/TLS (preferred) or STARTTLS, depending on what your server supports.
  • The port number will change automatically. If it doesn’t or your server does not use the default ports for encrypted transport, change the port number.
  • Select “Outgoing Server (SMTP)” (it’s down, below “Local Folders”).
  • Edit the entry you want to change (usually there is only one).
  • Change Connection Security to SSL/TLS (preferred) or STARTTLS, depending on what your server supports.
  • The port number will change automatically. If it doesn’t or your server does not use the default ports for encrypted transport, change the port number.

Now test it and have fun.

Fighting Secure Boot

 Windows, Windows 8.1  Kommentare deaktiviert
Okt 232014
 

I have bought an Acer Extensa notebook after reading the under 300 Euros notebooks test in the latest c’t magazine where it came up as the winner regarding battery life and the rest wasn’t too bad either. I chose the 4 GB model so it won’t thrash the hd all the time. The Extensa comes preinstalled with Windows 8.1 and – as so many computers nowadays comes without an install medium and also without a user’s manual. (The link given in the short setup guide for downloading the manual http://go.acer.com/?id=17833 leads to a non-functional site. Not very user friendly in my book.)

Now, what is the first thing you do, when you get a new computer which comes pre-installed with an operating system but does not come with an install medium? I for one, make a backup, preferably an image backup of the whole hard disk using Clonezilla. Since this is my image backup tool of choice I carry it with me on a USB stick almost all the time (Hey, I work in IT, so it’s pretty much normal to carry USB sticks and other stuff. ;-) ). So I plugged that USB stick into the notbook and booted it up. It went straight into the Windows 8.1 setup screen. :-(

So I tried to get a boot menu. Perusing Google told me that Acer notebooks use F12 for the boot menu. Unfortunately this didn’t work. Windows 8.1 setup again. :-(

Next, I tried to get into the BIOS, or whatever the UEFI stuff nowadays calls this tool. The usual DEL key didn’t work but after several reboots and key presses I ended up in some windows boot menu that allowed me to boot from an USB stick. Only, it didn’t. It told me there was a secure boot failure and stopped.

Turning the computer off and on again, this time I apparently got the BIOS setup key right: F2 (It didn’t work the first several times I tried it, why?) I got something called “Insydeh” which looked like a BIOS of old. And there it was: An option to turn off “secure boot”, only it was disabled. I could only switch to BIOS mode which I didn’t want to. WTF?

Google to the rescue again: To turn off secure boot, you first must set a supervisor password. So I did that, came back to the secure boot screen and lo and behold, the option to turn it off was enabled now. After turning it off, I could clear the supervisor password and the option was still enabled. Another setting I changed was the F12 boot menu. It was disabled by default so I enabled it.

Save and reboot, press F12 and – voila – a boot menu which finally allowed me to boot clonezilla from my USB stick. The backup is running now.

Praise Microsoft for requiring PC manufacturers to have an option to turn off secure boot if they want to be Windows 8 compliant (I wonder whether that will still be a requirement for Windows 10, though.). But curse Microsoft and the bloody PC manufacturers to come up with the pretty much useless secure boot feature at all. It’s my computer, I paid for it, so it should be my choice to install whatever operating system I want on it!