/* Copyright (C) 2005-2011 Fabio Riccardi */

/**
 * JavaAppLauncher: a simple Java application launcher for Windows.
 * main.cpp
 *
 * Paul J. Lucas [paul@lightcrafts.com]
 */

// standard
#include <cctype>
#include <cstdio>
#include <cstdlib>                      /* for exit(3) */
#include <cstring>
#include <fstream>
#include <io.h>                         /* for _open_osfhandle() */
#include <iostream>

// windows
#include <shlobj.h>
#include <tlhelp32.h>
#include <w32api.h>
#include <windows.h>
#include <winbase.h>

// local
#include "JavaParamBlock.h"
#include "LC_CPPUtils.h"
#include "LC_JNIUtils.h"
#include "LC_WinUtils.h"
#include "md5.h"
#include "StartJava.h"
#include "UI.h"

using namespace std;
using namespace LightCrafts;

/* Registers for 32-bit x86. */
#define CPU_REG_b "ebx"
#define CPU_REG_S "esi"

#define CPUID(index,eax,ebx,ecx,edx)                            \
        asm volatile (                                          \
            "mov %%" CPU_REG_b ", %%" CPU_REG_S "\n\t"          \
            "cpuid\n\t"                                         \
            "xchg %%" CPU_REG_b ", %%" CPU_REG_S                \
            : "=a" (eax), "=S" (ebx), "=c" (ecx), "=d" (edx)    \
            : "0" (index)                                       \
        )

//
// Obfuscate function names in object code.
//
#define checkCPUType        cC
#define checkJar            cJ
#define checkJars           cJs
#define checkWindowsVersion cW
#define rot47               r

// An acknowledgement message from the first running instance to the second.
char const ACK[] = "ACK";

// The maximum number of JVM arguments we allow.
int const MaxJVMArgs = 16;

// The size of the pipe buffer between running instances.
int const PipeBufSize = 1024;

static JavaParamBlock g_jpb;

/**
 * Checks whether the given window is a LightZone window.
 */
static bool isLightZoneWindow( HWND window ) {
    WCHAR wTextBuf[ 80 ];
    if ( !::GetWindowText( window, wTextBuf, sizeof wTextBuf / sizeof(WCHAR) ) )
        return false;
    if ( !::wcsstr( wTextBuf, TEXT("LightZone") ) )
        return false;
    DWORD pid;
    ::GetWindowThreadProcessId( window, &pid );
    return pid == ::GetCurrentProcessId();
}

/**
 * Find a LightZone window and bring it to the front.  This function is a
 * call-back for EnumWindows() and shouldn't be called directly.
 */
BOOL CALLBACK bringAppWindowToFront( HWND window, LPARAM ) {
    if ( !isLightZoneWindow( window ) )
        return TRUE;

    //
    // First, starting from the found window, traverse the stack of windows
    // looking for the one with the least Z-order (top-most).
    //
    for ( HWND w = window; w = ::GetNextWindow( w, GW_HWNDPREV ); )
        if ( isLightZoneWindow( w ) )
            window = w;

    //
    // Next, starting from the least Z-order window, traverse the list of
    // parent windows to find the eldest parent.
    //
    for ( HWND w = window; w = ::GetParent( w ); )
        if ( isLightZoneWindow( w ) )
            window = w;

    ::ShowWindow( window, SW_RESTORE ); // unminimize, if necessary

    HWND const fgWindow = ::GetForegroundWindow();
    if ( window != fgWindow ) {
#ifndef USE_SwitchToThisWindow
        //
        // Windows 98 and later deny background applications the ability to set
        // the foreground window.  Usually, this is a good thing since it
        // prevents background applications from popping-up a window and
        // stealing focus while the user is doing something else.
        //
        // However, in our case, the user either just double-clicked our
        // application icon or some other application requested that we open
        // some image files; hence, bringing our window to the front really is
        // the right thing to do.  Honest!
        //
        // To get around Windows' prohibition, we can trick it into thinking
        // this thread is the thread that currently owns the current foreground
        // window by attaching our input to it temporarily.  Once attached, we
        // can set the foreground window.  After setting it, we detach.
        //
        // However, there are potentially 3 threads involved: (1) the current
        // thread, (2) the thread that owns the LightZone window (which may or
        // may not be the current thread), and (3) the thread that owns the
        // foreground window.  We've discovered that the aforementioned
        // trickery works only if we attach both threads 1 and 2 to thread 3.
        //
        DWORD const curTID = ::GetCurrentThreadId();
        DWORD const lznTID = ::GetWindowThreadProcessId( window, NULL );
        DWORD const fgwTID = ::GetWindowThreadProcessId( fgWindow, NULL );

        if ( curTID != fgwTID )
            ::AttachThreadInput( curTID, fgwTID, TRUE );
        if ( lznTID != curTID && lznTID != fgwTID )
            ::AttachThreadInput( lznTID, fgwTID, TRUE );

        ::SetForegroundWindow( window );

        if ( curTID != fgwTID )
            ::AttachThreadInput( curTID, fgwTID, FALSE );
        if ( lznTID != curTID && lznTID != fgwTID )
            ::AttachThreadInput( lznTID, fgwTID, FALSE );
#else
        //
        // This code has been placed here for safe keeping.  It is an alternate
        // to the above if it ever stops working.  The reason for using the
        // above is because SwitchToThisWindow() has been deprecated.
        //
        SwitchToThisWindow( window, FALSE );
#endif  /* USE_SwitchToThisWindow */
    }

    return FALSE;                       // stop enumerating
}

/**
 * Check the CPU type.
 */
static void checkCPUType() {
    //
    // Before we can check for anything about the CPU, we first need to check
    // for cpuid instruction support.
    //
    long a, c;
    asm volatile (
        /* Copy EFLAGS into eax and ecx. */
        "pushf\n\t"
        "pop %0\n\t"
        "mov %0, %1\n\t"

        /* Toggle the ID bit in one copy and store to the EFLAGS register. */
        "xor $0x200000, %0\n\t"
        "push %0\n\t"
        "popf\n\t"

        /* Get the (hopefully modified) EFLAGS. */
        "pushf\n\t"
        "pop %0\n\t"
        : "=a" (a), "=c" (c)
        :
        : "cc"
    );
    if ( a == c )
        LC_die( LS_REQ_CPUID );

    //
    // We need to check for SSE2 instruction support.  We'd simply like to be
    // able to use:
    //
    //      IsProcessorFeaturePresent( PF_XMMI64_INSTRUCTIONS_AVAILABLE )
    //
    // but this isn't supported on Windows 2000 (non-Professional).
    //
    int max_std_level, std_caps;
    int eax, ebx, ecx, edx;

    CPUID( 0, max_std_level, ebx, ecx, edx );
    if ( max_std_level >= 1 ) {
        CPUID( 1, eax, ebx, ecx, std_caps );
        if ( std_caps & (1 << 26) /* SSE2 */ )
            return;
    }
    LC_die( LS_REQ_SSE2 );
}

/**
 * Check a jar to see if its hash still matches what it did at the time it was
 * built.
 */
static void checkJar( char const *jarPath, char const *correctMD5 ) {
    typedef unsigned char UCHAR;
    typedef UCHAR const UCHARC;

    int const MD5_BUF_SIZE = 16;        // MD5 encodes to 128 bits or 16 bytes

    //
    // The "salt" is the secret key.  First, it's broken into two big pieces
    // because it's better cryptographically speaking to put a salt both before
    // and after the actual data to be encrypted by MD5 since, even if you know
    // what the salt is, you don't know where the "split" between the two
    // pieces is.  (A null separates the 2 pieces.)
    //
    // Second, it's broken into 4-character chunks so the output of the Unix
    // "strings" command on the library shows at most the chunks and not the
    // entire salt as a single string.
    //
    char const *const SALT[] = {
        "Quid", "quid", " lat", "ine ", "dict", "um", 0,
        " sit", " alt", "um v", "idit", "ur", 0 
    };  

    //
    // This SEASON macro is used to add salt to the MD5 data.
    // Season ... salt: get it?  :-)
    //
#   define SEASON(STR) MD5Update( &md5_ctx, (UCHARC*)(STR), ::strlen( STR ) )

    MD5Context md5_ctx;
    MD5Init( &md5_ctx );

    char const *const *salt;
    for ( salt = SALT; *salt; ++salt )
        SEASON( *salt );

    ifstream ifs( jarPath, ios::binary );
    if ( !ifs )
        LC_die( TEXT("Missing jar.") );
    char buf[ 8192 ];
    while ( !ifs.eof() ) {
        ifs.read( buf, sizeof buf );
        MD5Update( &md5_ctx, reinterpret_cast<UCHARC*>( buf ), ifs.gcount() );
    }

    for ( ++salt; *salt; ++salt )
        SEASON( *salt );

    UCHAR md5_buf[ MD5_BUF_SIZE ];
    MD5Final( md5_buf, &md5_ctx );

    char md5_char_buf[ MD5_BUF_SIZE * 2 + 1 ];
    char *p = md5_char_buf;
    for ( int i = 0; i < MD5_BUF_SIZE; ++i, p += 2 )
        ::sprintf( p, "%02x", md5_buf[i] );
    *p = '\0';

    if ( ::strcmp( md5_char_buf, correctMD5 ) != 0 )
        LC_die( TEXT("Corrupted jar.") );
}

/**
 * ROT-47 cipher.
 */ 
static char* rot47( char *s, char const *s47 ) {
    char *const s0 = s;
    do {
        char c = *s47;
        if ( c >= '!' && c <= '~' )
            c = (c - '!' + 47) % 94 + '!';
        *s++ = c;
    } while ( *s47++ );
    return s0;  
}

/**
 * Ensure the jars haven't been altered since build-time.
 */
static void checkJars() {
    char const *const jarMD5[] = {
#       include "jar_md5_include"
        0
    };
    for ( char const *const *s = jarMD5; *s; s += 2 ) {
        char jarBuf[ 50 ];
        checkJar( rot47( jarBuf, s[0] ), s[1] );
    }
}

/**
 * Check the Windows version.
 */
static void checkWindowsVersion() {
    OSVERSIONINFOEX info;
    ::ZeroMemory( &info, sizeof( OSVERSIONINFOEX ) );
    info.dwOSVersionInfoSize = sizeof( OSVERSIONINFOEX );
    if ( !::GetVersionEx( reinterpret_cast<OSVERSIONINFO*>( &info ) ) )
        LC_die( TEXT("Unable to determine Windows version.") );
    cout << "JavaAppLauncher checkWindowsVersion()"
         << "\n    dwMajorVersion=" << info.dwMajorVersion
         << "\n    dwMinorVersion=" << info.dwMinorVersion
         << "\n    wServicePackMajor=" << info.wServicePackMajor << endl;
    //
    // dwMajorVersion:
    //  4 = Windows NT 4.0
    //  5 = Windows Server 2003, Windows 2000, or Windows XP
    //  6 = Windows Vista
    //
    if ( info.dwMajorVersion > 5 ) {
        //
        // Be optimistic and assume we'll run on Vista or anything later.
        //
        if ( info.dwMajorVersion == 6 )
            cout << "  = Windows Vista" << endl;
        return;
    }
    if ( info.dwMajorVersion == 5 ) {
        switch ( info.dwMinorVersion ) {
            case 0: // Windows 2000
                cout << "  = Windows 2000" << endl;
                if ( info.wServicePackMajor >= 4 )
                    return;
                break;
            case 1: // Windows XP
            case 2: // Windows XP Pro x64
                cout << "  = Windows XP";
                if ( info.dwMinorVersion == 2 )
                    cout << " x64";
                cout << endl;
                if ( info.wServicePackMajor >= 2 )
                    return;
                break;
            default:
                //
                // Assume any future minor versions are OK.
                //
                return;
        }
    }
    LC_die( LS_REQ_WIN_VER );
}

/**
 * Gets the name of our parent application's executable.  Note that the caller
 * of this function is responsible for delete[]'ing the string returned.
 */
static LPCWSTR getParentExe() {
    //
    // Take a snap-shot of all the processes.
    //
    HANDLE hSnapShot = ::CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 );
    if ( hSnapShot == INVALID_HANDLE_VALUE )
        return NULL;

    DWORD const pid = ::GetCurrentProcessId();
    DWORD ppid = 0;

    PROCESSENTRY32 pe;
    ::memset( &pe, 0, sizeof( PROCESSENTRY32 ) );
    pe.dwSize = sizeof( PROCESSENTRY32 );
    //
    // Iterate through all the processes in the snap-shot looking for this
    // process to get our parent proccess ID.
    //
    BOOL next = ::Process32First( hSnapShot, &pe );
    while ( next ) {
        if ( pid == pe.th32ProcessID ) {
            ppid = pe.th32ParentProcessID;
            break;
        }
        pe.dwSize = sizeof( PROCESSENTRY32 );
        next = ::Process32Next( hSnapShot, &pe );
    }

    LPWSTR wParentExe = NULL;
    if ( ppid ) {
        ::memset( &pe, 0, sizeof( PROCESSENTRY32 ) );
        pe.dwSize = sizeof( PROCESSENTRY32 );
        //
        // Now iterate through all the processes again looking for our
        // parent process ID so we can get its exe name.
        //
        next = ::Process32First( hSnapShot, &pe );
        while ( next ) {
            if ( ppid == pe.th32ProcessID ) {
                wParentExe = new WCHAR[ ::wcslen( pe.szExeFile ) + 1 ];
                ::wcscpy( wParentExe, pe.szExeFile );
                break;
            }
            pe.dwSize = sizeof( PROCESSENTRY32 );
            next = ::Process32Next( hSnapShot, &pe );
        }
    }

    ::CloseHandle( hSnapShot );
    return wParentExe;
}

/**
 * Open the given file by calling a call-back method on the Java side.
 */
static void openFile( LPCWSTR wPathToFile, LPCWSTR wParentExe ) {
#ifdef DEBUG
    cout << "*** In openFile()" << endl;
#endif
    JNIEnv *const env = LC_attachCurrentThread();

    static jmethodID openFile_methodID;
    if ( !openFile_methodID ) {
        openFile_methodID = env->GetStaticMethodID(
            g_jpb.main_class, "openFile",
            "(Ljava/lang/String;Ljava/lang/String;)V"
        );
        if ( !openFile_methodID )
            LC_die( TEXT("Could not find openFile() method.") );
    }

    char aPathToFile[ MAX_PATH ];
    if ( !LC_toUTF8( wPathToFile, aPathToFile, sizeof aPathToFile ) )
        LC_die( TEXT("WideCharToMultiByte() failed.") );
    jstring jPathToFile = env->NewStringUTF( aPathToFile );
    if ( !jPathToFile )
        LC_die( TEXT("NewStringUTF() failed.") );

    jstring jParentExe = NULL;
    if ( wPathToFile && *wPathToFile ) {
        char aParentExe[ 80 ];
        if ( !LC_toUTF8( wParentExe, aParentExe, sizeof aParentExe ) )
            LC_die( TEXT("WideCharToMultiByte() failed.") );
        jParentExe = env->NewStringUTF( aParentExe );
        if ( !jParentExe )
            LC_die( TEXT("NewStringUTF() failed.") );
    }

    env->CallStaticVoidMethod(
        g_jpb.main_class, openFile_methodID, jPathToFile, jParentExe
    );
}

/**
 * Open one or more files on the given command-line.
 */
static void openFiles( LPCWSTR wCommandLine, LPCWSTR wParentExe ) {
#ifdef DEBUG
    cout << "*** In openFiles()" << endl;
#endif
    int argc;
    LPWSTR *const wArgv = ::CommandLineToArgvW( wCommandLine, &argc );
    if ( wArgv ) {
        //
        // Start at wArgv[1] because wArgv[0] is the name of the executable
        // (just like in Unix).
        //
        for ( int i = 1; i < argc; ++i )
            openFile( wArgv[i], wParentExe );
        ::LocalFree( wArgv );
    }
}

/**
 * Remove all leading and trailing whitespace from the given string and return
 * it.  The string is modified in-place.
 */
static char* trim( char *s ) {
    while ( isspace( *s ) )
        ++s;
    for ( int i = ::strlen( s ) - 1; i >= 0; --i )
        if ( isspace( s[i] ) )
            s[i] = '\0';
    return s;
}

/**
 * Read the JVM arguments, one per line, from a text file.  Comments and blank
 * lines are ignored.
 */
static char const** readJVMArgs() {
    static char const *jvmArgs[ MaxJVMArgs ];

    ifstream argsFile( "lightzone.jvmargs" );
    if ( !argsFile )
        LC_die( TEXT("Could not open lightzone.jvmargs file.") );

    char buf[ 1024 ];
    int i = 0;
    while ( argsFile.getline( buf, sizeof buf ) ) {
        char const *const arg = trim( buf );
        if ( !*arg || *arg == '#' )
            continue;
        if ( i >= MaxJVMArgs )
            LC_die( TEXT("Too many JVM arguments.") );
        jvmArgs[i++] = new_strdup( arg );
    }
    argsFile.close();
    return jvmArgs;
}

/**
 * Redirect standard output and standard error to a log file.
 */
static void redirectOutput() {
    //
    // Get the path to the "My Documents" folder (pre-Vista) or "Documents"
    // folder (Vista).
    //
    WCHAR wLogFile[ MAX_PATH ];
    HRESULT hResult = ::SHGetFolderPath(
        NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, wLogFile
    );
    if ( FAILED( hResult ) )
        return;

    //
    // Create the directory (if necessary) where the log file will go.
    //
    LPCWSTR const wAppName = LC_getAppName();
    ::wcscat( wLogFile, TEXT("\\") );
    ::wcscat( wLogFile, wAppName );
    if ( !LC_makeDir( wLogFile ) )
        return;

    //
    // Complete the full path of the log file.
    //
    ::wcscat( wLogFile, TEXT("\\") );
    ::wcscat( wLogFile, wAppName );
    ::wcscat( wLogFile, TEXT(".log") );

    //
    // Attempt to create the log file.
    //
    HANDLE logHandle = ::CreateFile(
        wLogFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS,
        FILE_ATTRIBUTE_NOT_CONTENT_INDEXED | FILE_FLAG_WRITE_THROUGH, NULL
    );
    if ( logHandle == INVALID_HANDLE_VALUE ) {
        //
        // An instance of the application might already be running in which
        // case the log file would already be open for writing exclusively to
        // the first running instance.  Trying to create the log file in this
        // case will result in an ERROR_SHARING_VIOLATION.  If that's the case,
        // just return without either creating or redirecting to the log file.
        //
        DWORD const errorCode = ::GetLastError();
        if ( errorCode == ERROR_SHARING_VIOLATION )
            return;
        //
        // For any other error, however, die.
        //
        LC_die( TEXT("Create log file"), errorCode );
    }

    //
    // Redirect stdout and stderr to the log file.  Note, however, that this
    // will only redirect Java's System.out and System.err.
    //
    if ( !::SetStdHandle( STD_OUTPUT_HANDLE, logHandle ) )
        LC_die( TEXT("Redirect to standard output"), ::GetLastError() );
    if ( !::SetStdHandle( STD_ERROR_HANDLE, logHandle ) )
        LC_die( TEXT("Redirect to standard error"), ::GetLastError() );

    //
    // To redirect C's stdout and stderr as well as C++'s cout and cerr to the
    // same log file, we have to convert logHandle to a FILE* then replace
    // stdout and stderr with it.
    //
    int const logFD = _open_osfhandle( reinterpret_cast<long>( logHandle ), 0 );
    FILE *const logFile = ::_fdopen( logFD, "w" );
    ::setvbuf( logFile, NULL, _IONBF, 0 );
    ::fclose( stdout ); *stdout = *logFile;
    ::fclose( stderr ); *stderr = *logFile;
}

/**
 * Run our application.
 */
int APIENTRY WinMain( HINSTANCE, HINSTANCE, LPSTR, int ) {
    redirectOutput();
    checkWindowsVersion();
    checkCPUType();

    if ( !::SetCurrentDirectory( LC_getExeDirectory() ) )
        LC_die( TEXT("Set current directory"), ::GetLastError() );

    checkJars();

    LPCWSTR const wCommandLine = ::GetCommandLine();

    ///////////////////////////////////////////////////////////////////////////

    //
    // Create a named pipe to use for communication between the second and the
    // first running application instances.
    //
    // When a second instance is launched by Windows (either by the user
    // double-clicking the LightZone.exe icon or by the user dragging image
    // files onto the LightZone.exe icon), the second instance detects that
    // it is a second instance and then sends the files to open to the first
    // instance via the pipe.
    //
    // By specifying FILE_FLAG_FIRST_PIPE_INSTANCE and maxInstances of 1, the
    // first instance successfully creates the pipe.  Secondary instances will
    // fail because the pipe already exists and thus know they're secondary
    // instances.
    //
    WCHAR wPipeName[ 32 ] = TEXT("\\\\.\\pipe\\");
    ::wcscat( wPipeName, LC_getAppName() );
    HANDLE pipe = ::CreateNamedPipe(
        wPipeName,
        PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE,
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
        1,                              // maxInstances
        sizeof ACK,                     // output buffer size
        PipeBufSize,                    // input buffer size
        NMPWAIT_USE_DEFAULT_WAIT,
        NULL
    );
    if ( pipe == INVALID_HANDLE_VALUE ) {
        DWORD const error = ::GetLastError();
        if ( error == ERROR_ACCESS_DENIED || error == ERROR_PIPE_BUSY ) {
            //
            // We're a second instance of the appplication.
            //
            int const wCommandLineLen = ::wcslen( wCommandLine );
            if ( wCommandLineLen ) {
                //
                // We have files to open: send our command-line to the first
                // instance to have it open the files instead of us.
                //
                LPWSTR wPipeBuf;
                int wPipeBufLen;

                if ( LPCWSTR const wParentExe = getParentExe() ) {
                    //
                    // We have a parent process that requested the files to be
                    // opened: prepend the name of the parent process's exe
                    // followed by a ':' to the command-line.
                    //
                    int const wParentExeLen = ::wcslen( wParentExe );
                    wPipeBufLen = wParentExeLen + 1 + wCommandLineLen;
                    wPipeBuf = new WCHAR[ wPipeBufLen + 1 /* for null */ ];
                    ::wcscpy( wPipeBuf, wParentExe );
                    ::wcscat( wPipeBuf, TEXT(":" ) );
                    ::wcscat( wPipeBuf, wCommandLine );
                    //
                    // Note: we don't care about delete[]'ing wParentExe since
                    // this process is going to die very soon.
                    //
                } else {
                    //
                    // No parent process (I don't think this is ever the case
                    // under Windows, but it's defensive programming to assume
                    // that it can happen): just send the command-line as-is.
                    //
                    wPipeBuf = const_cast<LPWSTR>( wCommandLine );
                    wPipeBufLen = wCommandLineLen;
                }

                char ackBuf[ sizeof ACK ];
                DWORD bytesRead;
                BOOL const sent = ::CallNamedPipe(
                    wPipeName, wPipeBuf,
                    (wPipeBufLen + 1 /* include null */) * sizeof( WCHAR ),
                    ackBuf, sizeof ackBuf, &bytesRead, NMPWAIT_USE_DEFAULT_WAIT
                );
                if ( !sent )
                    LC_die(
                        TEXT("Communication with running instance"),
                        ::GetLastError()
                    );
            } else {
                //
                // We have no files to open (the user must have just double-
                // clicked the application icon or a shortcut thereto), so just
                // bring the top-most window to the front.
                //
                ::EnumWindows( &bringAppWindowToFront, 0 );
            }
            ::exit( 0 );                // There can be only one....
        }
        LC_die( TEXT("CreateNamedPipe()"), error );
    }

    ///////////////////////////////////////////////////////////////////////////

    //
    // If we get here, we're the first instance of the application: fire up a
    // JVM.
    //
    initJavaParamBlock( &g_jpb, readJVMArgs() );
    startJava( &g_jpb );

    //
    // If we were given any files to open, open them.
    //
    LPCWSTR wParentExe = getParentExe();
    openFiles( wCommandLine, wParentExe );
    delete[] wParentExe;

    ///////////////////////////////////////////////////////////////////////////

    //
    // Now wait for secondary application instances to connect to us and read
    // their command-lines and open the files they specify.
    //
    while ( true ) {
        BOOL const connected = ::ConnectNamedPipe( pipe, NULL ) ?
            TRUE : (::GetLastError() == ERROR_PIPE_CONNECTED);
        if ( connected ) {
            ::EnumWindows( &bringAppWindowToFront, 0 );

            WCHAR wPipeBuf[ PipeBufSize ];
            DWORD byteCount;
            BOOL success = ::ReadFile(
                pipe, wPipeBuf, sizeof wPipeBuf, &byteCount, NULL
            );
            if ( success ) {
                LPCWSTR wCommandLine, wParentExe;
                //
                // We got a command-line from a secondary application instance:
                // first see if it's preceeded by an exe name.
                //
                WCHAR *const colon = ::wcschr( wPipeBuf, TEXT(':') );
                if ( colon ) {
                    wParentExe = wPipeBuf;
                    wCommandLine = colon + 1;
                    *colon = 0;
                } else {
                    wParentExe = NULL;
                    wCommandLine = wPipeBuf;
                }

                openFiles( wCommandLine, wParentExe );
                //
                // Even though we have no need to send anything back to the
                // second instance, apparently Windows isn't happy unless we do
                // (the second instance will get an ERROR_PIPE_NOT_CONNECTED
                // error), so just send it a tiny packet containing "ACK".
                //
                success = ::WriteFile(
                    pipe, ACK, sizeof ACK, &byteCount, NULL
                );
                if ( !success )
                    LC_warn(
                        TEXT("Instance acknowledgement"), ::GetLastError()
                    );
            } else
                LC_warn( TEXT("Opening files"), ::GetLastError() );
        }
        ::FlushFileBuffers( pipe );
        ::DisconnectNamedPipe( pipe );
    }

    return 0;
}

/* vim:set et sw=4 ts=4: */
