/**********************************************************************
File name: filewait.c

wait for change in specified file

	blame: shardy@@differentchairs.com
	(derived from Windows sdk fwatch sample.
	 Test build with VC6.0, and tested on WindowsXP)
**********************************************************************/
#define UNICODE

#define _WIN32_WINNT 0x400

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <windows.h>

#define MAX_BUFFER  4096

// this is the all purpose structure that contains
// the directory information of interest and provides
// the input buffer that gets filled with file change data

typedef struct _DIRECTORY_INFO {
	HANDLE      hDir;
	TCHAR       WatchedFileName[MAX_PATH];
	CHAR        lpBuffer[MAX_BUFFER];
	DWORD       dwBufLength;
	OVERLAPPED  Overlapped;
} DIRECTORY_INFO, *PDIRECTORY_INFO, *LPDIRECTORY_INFO;

DIRECTORY_INFO	DirInfo;
BOOL			Done = FALSE;

/**********************************************************************
CheckChangedFile()

Purpose:
	Indicates whether file notification matches our watched file.

Parameters:
	lpfni - Information about modified file
	WatchedFileName - compare notified filename against this

Return Value:
	TRUE if matched
********************************************************************/

BOOL WINAPI CheckChangedFile( PFILE_NOTIFY_INFORMATION lpfni,
							  TCHAR* WatchedFileName)
{
	BOOL result = FALSE;
	TCHAR      *p;
	unsigned i, len = lstrlen(WatchedFileName);

	if (len!=lpfni->FileNameLength/sizeof(WCHAR))
	{
		p=0;
	}
	else {
		p = WatchedFileName;
		for (i=0; i<len; i++)
		{
			if (towupper(p[i])!=towupper(lpfni->FileName[i]))
			{
				p=0;
				break;
			}
		}
	}
	if(p && *p)     {
		result = TRUE;
		}
	return result;
	}


/**********************************************************************
HandleDirectoryChanges()

Purpose:
	This function receives notification of directory changes and calls
	CheckChangedFile() to determine if the change was in our watched file.

	Calls ReadDirectoryChangesW to reestablish the watch, if needed.

Parameters:
	HANDLE dwCompletionPort - Handle for completion port

Return Value:
	None
********************************************************************/
void WINAPI HandleDirectoryChange( DWORD dwCompletionPort ) {
	DWORD numBytes;
	DWORD cbOffset;
	LPDIRECTORY_INFO di = 0;
	LPOVERLAPPED lpOverlapped;
	PFILE_NOTIFY_INFORMATION fni;
	do     {
		// Retrieve the directory info for this directory
		// through the completion key
		BOOL x = GetQueuedCompletionStatus( (HANDLE) dwCompletionPort,
									&numBytes,
									(LPDWORD) &di,
									&lpOverlapped,
									INFINITE);
		if ( di )
		{
			fni = (PFILE_NOTIFY_INFORMATION)di->lpBuffer;
			do             {
				cbOffset = fni->NextEntryOffset;
				if( fni->Action == FILE_ACTION_MODIFIED )
					Done=CheckChangedFile( fni, (TCHAR*)di->WatchedFileName );
				fni = (PFILE_NOTIFY_INFORMATION)((LPBYTE) fni + cbOffset);
			} while( !Done && cbOffset );
			if (!Done) // Reissue the watch command
				ReadDirectoryChangesW( di->hDir,di->lpBuffer,
					MAX_BUFFER,
					TRUE,
					FILE_NOTIFY_CHANGE_LAST_WRITE,
					&di->dwBufLength,
					&di->Overlapped,
					NULL);
		}
	} while( di && !Done);
}

/**********************************************************************
WatchDirectories()

Purpose:
	This function inovkes the ReadDirectoryChangesW API for
	directory associated with given IOCompletionPort. A thread
	is created which waits for changes to the directory. This
	function waits until the watched file changes, or the timeout elapses.

Parameters:
	HANDLE hCompPort - Handle for completion port
	timeout the maximum time to wait (in ms) for file change

Return Value:
				None
********************************************************************/
void WINAPI WatchDirectories( HANDLE hCompPort, DWORD timeout ) {
	DWORD   tid;
	HANDLE  hThread;

	// Start watching the directory of interest
	ReadDirectoryChangesW(
			DirInfo.hDir,
			DirInfo.lpBuffer,
			MAX_BUFFER,
			TRUE,
			FILE_NOTIFY_CHANGE_LAST_WRITE,
			&DirInfo.dwBufLength,
			&DirInfo.Overlapped,
			NULL);

	// Create a thread to watch directory changes
	hThread = CreateThread( NULL,
			0,
			(LPTHREAD_START_ROUTINE) HandleDirectoryChange,
			hCompPort,
			0,
			&tid);

	if (WAIT_TIMEOUT==WaitForSingleObject( hThread, timeout ))
	{
		PostQueuedCompletionStatus( hCompPort, 0, 0, NULL );
		// Wait for the watcher thread to finish before exiting
		WaitForSingleObject( hThread, INFINITE );
	}
	CloseHandle( hThread );
}

void help()
{
	printf("\nfilewait sleeps until target file changes or optional ");
	printf("\ntimer (in ms) elapses. Return 0 if file change detected.\n");
	printf("\nusage: filewait [-t <n>] <filename>\n");
}

/**********************************************************************
main()

Command usage:
	filewait [-t <n>] <filename>


Test:
	::::::::
	@echo off
	::
	:: test_filewait.cmd
	::
	setlocal ENABLEDELAYEDEXPANSION
	set testfail=
	set testcases=c:\dir1\file.txt file.txt c:file.txt \file.txt c:file.txt c:\file.txt

	echo.
	echo filewait TEST
	echo.
	echo testcases: +/-[%testcases%]

	:: test notify on file changed
	start "testcase file updates" cmd /c"(for %%a in (%testcases%) do sleep 2 & echo x >%%a) "
	for %%a in (%testcases%) do (
		filewait %%a -t 3000 > nul || (
			echo FAIL+[%%a]
			set testfail=!testfail!.FAIL+[%%a]
			)
	)

	:: test timeout on different/sibling file changed
	for %%a in (%testcases%) do (
		(start "update [%%a.X]" cmd /c"sleep 2 &echo x>%%a.X")
		filewait %%a -t 3000 > nul && (
			echo FAIL-[%%a]
			set testfail=!testfail!.FAIL-[%%a]
		)
	)

	echo.
	if defined testfail (
		echo %testfail%
	) else (
		echo PASS
	)
	endlocal
	::::::::

Return Value:
	0 file change detected
	1 no change detected before timeout elapsed
	2 error. (e.g., invalid filespec)
********************************************************************/
int main(int argc, char *argv[]) {
	HANDLE  hCompPort=NULL;
	int   i;
	TCHAR   FilePath[MAX_PATH];
	TCHAR   DirName[MAX_PATH];
	HANDLE  hFile;

	TCHAR drive[_MAX_DRIVE];
	TCHAR dir[_MAX_DIR];
	TCHAR fname[_MAX_FNAME];
	TCHAR ext[_MAX_EXT];
	DWORD timeout = INFINITE;
	#define ERROR_DIR 2

	TCHAR watchfile[MAX_PATH+1];
	watchfile[0] = 0;

	if (2==argc || 4==argc)
		for (i=1; i<argc; i++)
		{
			if (0==strcmpi(argv[i],"/t") ||
				0==strcmpi(argv[i],"-t"))
			{
				timeout = atol(argv[++i]);
			}
			else
			{
				int j, len=strlen(argv[i]);
				char *fname=argv[i];
				for (j=0; j<len; j++)
				{
					watchfile[j] = fname[j];
				}
				watchfile[j] = 0;
			}
		}

	if (lstrlen(watchfile)==0)
	{
		help();
	}
	else
	{
		_wsplitpath( watchfile, drive, dir, fname, ext );

		if (0==lstrlen(drive) && 0==lstrlen(dir))
		{
			GetCurrentDirectory( MAX_PATH, DirName );
		}
		else
		{
			lstrcpy(DirName, drive);
			lstrcat(DirName, dir);
		}

		lstrcpy(DirInfo.WatchedFileName, fname);
		lstrcat(DirInfo.WatchedFileName, ext);

		lstrcpy(FilePath, DirName);
		lstrcat(FilePath, DirInfo.WatchedFileName);

		if( CreateDirectory( DirName, NULL ) )
			wprintf( L"Directory %s created\n", DirName );
		else
			wprintf( L"Directory %s exists\n", DirName );
		// Get a handle to the directory
		DirInfo.hDir = CreateFile( DirName,
				FILE_LIST_DIRECTORY,
				FILE_SHARE_READ |
				FILE_SHARE_WRITE |
				FILE_SHARE_DELETE,
				NULL,
				OPEN_EXISTING,
				FILE_FLAG_BACKUP_SEMANTICS |
				FILE_FLAG_OVERLAPPED,
				NULL);

		if( DirInfo.hDir == INVALID_HANDLE_VALUE )         {
			wprintf( L"Unable to open directory %s. GLE=%d. Terminating...\n",
					DirName, GetLastError() );
			exit( ERROR_DIR );
		}
		if( INVALID_HANDLE_VALUE !=
			(hFile = CreateFile( FilePath,
				GENERIC_WRITE,
				FILE_SHARE_READ ,
				NULL,
				CREATE_NEW,
				FILE_ATTRIBUTE_NORMAL,
				NULL)) )
		{
			wprintf( L"  File %s created\n", FilePath );
			CloseHandle( hFile );
		}
		else if( INVALID_HANDLE_VALUE !=
			(hFile = CreateFile( FilePath,
				GENERIC_READ,
				FILE_SHARE_READ ,
				NULL,
				OPEN_EXISTING,
				FILE_ATTRIBUTE_NORMAL,
				NULL)) )
		{
			wprintf( L"  File [%s] exists\n", FilePath );
			CloseHandle( hFile );
		}
		else // allow timeout...
			wprintf( L"  File [%s] could not be created\n", FilePath );

		hCompPort=CreateIoCompletionPort( DirInfo.hDir, hCompPort, (DWORD) &DirInfo, 0);
		wprintf( L"\n\nwait...\n" );
		// Start watching
		WatchDirectories( hCompPort, timeout );
		CloseHandle( DirInfo.hDir );
		CloseHandle( hCompPort );
	}
	return !Done;
}