summaryrefslogtreecommitdiffstats
path: root/private/sdktools/ftc
diff options
context:
space:
mode:
authorAdam <you@example.com>2020-05-17 05:51:50 +0200
committerAdam <you@example.com>2020-05-17 05:51:50 +0200
commite611b132f9b8abe35b362e5870b74bce94a1e58e (patch)
treea5781d2ec0e085eeca33cf350cf878f2efea6fe5 /private/sdktools/ftc
downloadNT4.0-e611b132f9b8abe35b362e5870b74bce94a1e58e.tar
NT4.0-e611b132f9b8abe35b362e5870b74bce94a1e58e.tar.gz
NT4.0-e611b132f9b8abe35b362e5870b74bce94a1e58e.tar.bz2
NT4.0-e611b132f9b8abe35b362e5870b74bce94a1e58e.tar.lz
NT4.0-e611b132f9b8abe35b362e5870b74bce94a1e58e.tar.xz
NT4.0-e611b132f9b8abe35b362e5870b74bce94a1e58e.tar.zst
NT4.0-e611b132f9b8abe35b362e5870b74bce94a1e58e.zip
Diffstat (limited to '')
-rw-r--r--private/sdktools/ftc/app.rc10
-rw-r--r--private/sdktools/ftc/ftc.cxx1324
-rw-r--r--private/sdktools/ftc/makefile6
-rw-r--r--private/sdktools/ftc/sources18
4 files changed, 1358 insertions, 0 deletions
diff --git a/private/sdktools/ftc/app.rc b/private/sdktools/ftc/app.rc
new file mode 100644
index 000000000..f5958c45e
--- /dev/null
+++ b/private/sdktools/ftc/app.rc
@@ -0,0 +1,10 @@
+#include <windows.h>
+#include <ntverp.h>
+
+#define VER_FILETYPE VFT_APP
+#define VER_FILESUBTYPE VFT2_UNKNOWN
+#define VER_FILEDESCRIPTION_STR "Fast tree copy"
+#define VER_INTERNALNAME_STR "ftc\0"
+#define VER_ORIGINALFILENAME_STR "ftc.exe"
+
+#include <common.ver>
diff --git a/private/sdktools/ftc/ftc.cxx b/private/sdktools/ftc/ftc.cxx
new file mode 100644
index 000000000..50f844dcf
--- /dev/null
+++ b/private/sdktools/ftc/ftc.cxx
@@ -0,0 +1,1324 @@
+//+-------------------------------------------------------------------------
+//
+// Microsoft Windows
+// Copyright (C) Microsoft Corporation, 1996 - 1996.
+//
+// File: ftc.cxx
+//
+// Contents: Fast multi-threaded tree copy program.
+//
+// History: ?-?-94 IsaacHe Created
+// 11-Jun-96 BruceFo Fixed bugs, put this header here.
+//
+//--------------------------------------------------------------------------
+
+#if defined( UNICODE )
+#undef UNICODE
+#endif
+
+#include <windows.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <malloc.h>
+#include <direct.h>
+#include <sys\types.h>
+#include <sys\stat.h>
+#include <io.h>
+#include <conio.h>
+#include <errno.h>
+#include <process.h>
+#include <ctype.h>
+
+#define MAXQUEUE 10000
+#define ARRAYLEN(x) (sizeof(x) / sizeof((x)[0]))
+
+/*
+ * These are the attributes we use to compare for file attribute identity
+ */
+const DWORD FILE_ATTRIBUTE_MASK = FILE_ATTRIBUTE_READONLY |
+ FILE_ATTRIBUTE_HIDDEN |
+ FILE_ATTRIBUTE_SYSTEM |
+ FILE_ATTRIBUTE_ARCHIVE;
+
+class CProtectedLong
+{
+ CRITICAL_SECTION _cs;
+ LONG _value;
+
+public:
+ CProtectedLong() { InitializeCriticalSection( &_cs ); _value = 0; }
+ LONG operator++(int) {
+ EnterCriticalSection( &_cs );
+ LONG tmp = _value++;
+ LeaveCriticalSection( &_cs );
+ return tmp;
+ }
+ LONG operator--( int ) {
+ EnterCriticalSection( &_cs );
+ LONG tmp = _value--;
+ LeaveCriticalSection( &_cs );
+ return tmp;
+ }
+ LONG operator+=( LONG incr ) {
+ EnterCriticalSection( &_cs );
+ LONG tmp = (_value += incr );
+ LeaveCriticalSection( &_cs );
+ return tmp;
+ }
+ LONG operator=( LONG val ) {
+ EnterCriticalSection( &_cs );
+ _value = val;
+ LeaveCriticalSection( &_cs );
+ return val;
+ }
+ operator LONG() { return _value; }
+ operator int() { return _value; }
+};
+
+class CHandle
+{
+ HANDLE _h;
+
+public:
+ CHandle() : _h(INVALID_HANDLE_VALUE) { }
+ ~CHandle();
+ HANDLE operator=( HANDLE h );
+ BOOL operator==( HANDLE h ) { return (h == _h) ? TRUE : FALSE; }
+ BOOL operator!=( HANDLE h ) { return (h != _h) ? TRUE : FALSE; }
+ operator HANDLE() { return _h; }
+};
+
+CHandle::~CHandle()
+{
+ if( _h != INVALID_HANDLE_VALUE && _h != NULL )
+ CloseHandle( _h );
+}
+
+HANDLE
+CHandle::operator=( HANDLE h )
+{
+ if( _h != INVALID_HANDLE_VALUE && _h != NULL )
+ CloseHandle( _h );
+ return _h = h;
+}
+
+DWORD dwElapsedTime; // time we've been copying data
+CProtectedLong ulTotalBytesCopied; // running total count of bytes
+CProtectedLong ulTotalBytesSkipped; // obvious?
+CProtectedLong ulTotalBytesScanned;
+CProtectedLong nFilesOnQueue; // number of files to copy or examine
+CProtectedLong nFcopy; // number of files copied
+CProtectedLong nSkipped; // number of files skipped over
+CProtectedLong nMappedCopy; // number of files copied using MapFile...
+CProtectedLong nCopyFile; // number of files copied using CopyFile()...
+CProtectedLong nInProgress; // number of copies currently in progress
+CProtectedLong MaxThreads; // Max number of threads for copying
+DWORD ExitCode = 0; // each thread's exit code
+
+BOOL bThreadStop = FALSE; // are we trying to exit?
+BOOL bWorkListComplete = FALSE; // have we scanned all the directories yet?
+
+BOOL tFlag = FALSE; // only copy if newer
+BOOL iFlag = FALSE; // skip seemingly identical files
+BOOL rFlag = FALSE; // replace read-only files
+BOOL vFlag = FALSE; // verbose
+BOOL AFlag = FALSE; // keep going even if there are errors
+BOOL FFlag = FALSE; // just produce file list. No copies
+BOOL oFlag = TRUE; // should we overwrite files already at dest?
+BOOL wFlag = FALSE; // should we wait for the source to show up?
+BOOL qFlag = FALSE; // quiet mode?
+BOOL yFlag = FALSE; // no recurse on target?
+BOOL zFlag = FALSE; // no recurse on source?
+
+BOOL pFlag = FALSE; // pattern?
+CHAR szPattern[100]; // pattern string, if pFlag is TRUE
+
+struct WorkList // copy file at 'src' to 'dest'
+{
+ struct WorkList *next;
+ char *src; // Pathname relative to the source
+ WIN32_FIND_DATA srcfind;
+ char *dest;
+} *WorkList = NULL;
+struct WorkList* WorkListTail = NULL; // always add files to copy to the *tail*
+ // of the work list. This is to make
+ // sure we always do work in order, instead
+ // of starting on a directory but finishing
+ // it much much much later, because we've
+ // pushed all the work to the deep tail of
+ // the list and never returned to it!
+
+CHandle hWorkAvailSem; // signalled whenever there's work on the list
+CHandle hMaxWorkQueueSem; // used to control lenght of work queue
+
+CRITICAL_SECTION csMsg; // used to serialize screen output
+CRITICAL_SECTION csWorkList; // used when manipulating the linked list
+CRITICAL_SECTION csSourceList; // used when manipulating the source list
+
+struct SourceList
+{
+ char *name; // pathname of the source. Ends in '\'
+ LONG count; // number of files currently being copied
+ LONG ulTotalFiles; // total for the entire copy
+ struct {
+ unsigned valid : 1; // do we know that the source is valid?
+ } flags;
+} SourceList[ 20 ];
+int MaxSources = 0;
+
+char *DirectoryExcludeList[ 50 ];
+int MaxDirectoryExcludes = 0;
+
+char OldConsoleTitle[ 100 ];
+
+void
+errormsg( char const *pszfmt, ... )
+{
+ va_list ArgList;
+ va_start( ArgList, pszfmt );
+
+ if( bThreadStop == FALSE && pszfmt != NULL ) {
+ EnterCriticalSection( &csMsg );
+ vprintf( pszfmt, ArgList );
+ LeaveCriticalSection( &csMsg );
+ }
+
+ va_end( ArgList );
+}
+
+DWORD __stdcall
+StatusWorker( void *arg )
+{
+ char ostatbuf[ 100 ];
+ char nstatbuf[ 100 ];
+
+ while( bThreadStop == FALSE ) {
+ sprintf(nstatbuf,
+ "Remaining Files %d Bytes %d",
+ (int)nFilesOnQueue,
+ (int)ulTotalBytesScanned -
+ (int)ulTotalBytesCopied - (int)ulTotalBytesSkipped );
+
+ if( strcmp( ostatbuf, nstatbuf ) ) {
+ SetConsoleTitle( nstatbuf );
+ strcpy( ostatbuf, nstatbuf );
+ }
+ Sleep( 1 * 1000 );
+ }
+
+ SetConsoleTitle( OldConsoleTitle );
+ ExitThread( ExitCode );
+ arg = arg;
+ return 0;
+}
+
+void
+msg( char const *pszfmt, ... )
+{
+ if( qFlag )
+ return;
+
+ va_list ArgList;
+ va_start( ArgList, pszfmt );
+
+ EnterCriticalSection( &csMsg );
+ vprintf( pszfmt, ArgList );
+ LeaveCriticalSection( &csMsg );
+ va_end( ArgList );
+}
+
+void
+errorexit (char const *pszfmt, ... )
+{
+
+ if( bThreadStop == FALSE && pszfmt != NULL ) {
+ va_list ArgList;
+ va_start( ArgList, pszfmt );
+
+ EnterCriticalSection( &csMsg );
+ vprintf( pszfmt, ArgList );
+ LeaveCriticalSection( &csMsg );
+
+ va_end( ArgList );
+ }
+
+ if( AFlag == FALSE ) {
+ bThreadStop = TRUE;
+
+ EnterCriticalSection( &csWorkList );
+ WorkList = NULL;
+ WorkListTail = NULL;
+ LeaveCriticalSection( &csWorkList );
+
+ if( hWorkAvailSem != NULL )
+ ReleaseSemaphore( hWorkAvailSem, (int)MaxThreads+1, NULL );
+
+ if( hMaxWorkQueueSem != NULL )
+ ReleaseSemaphore( hMaxWorkQueueSem, 1, NULL );
+
+ SetConsoleTitle( OldConsoleTitle );
+ ExitThread (ExitCode = 1);
+ }
+}
+
+DWORD
+fcopy( char *src, WIN32_FIND_DATA *srcfind, char *dst, char *errorbuf )
+{
+ CHandle srcfh;
+ CHandle dstfh;
+ CHandle hsrc;
+ DWORD nBytesWritten, totalbytes;
+ char *result = NULL;
+ char *psrc;
+ BOOL ret = TRUE;
+ char dostatus = 0;
+ DWORD errcode;
+
+ *errorbuf = '\0';
+
+ if( srcfind->nFileSizeHigh != 0 ) {
+ if( CopyFile( src, dst, TRUE ) == FALSE ) {
+ errcode = GetLastError();
+ sprintf( errorbuf, "CopyFile failed, error %d", errcode );
+ return errcode;
+ }
+ nCopyFile++;
+
+ } else {
+
+ srcfh = CreateFile( src, GENERIC_READ, FILE_SHARE_READ, NULL,
+ OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL );
+ if( srcfh == INVALID_HANDLE_VALUE ) {
+ errcode = GetLastError();
+ sprintf( errorbuf, "Unable to open source file, error %d", errcode);
+ return errcode;
+ }
+
+ dstfh = CreateFile( dst, GENERIC_WRITE, FILE_SHARE_WRITE,NULL,
+ CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, srcfh);
+
+ if( dstfh == INVALID_HANDLE_VALUE ) {
+ errcode = GetLastError();
+ sprintf( errorbuf, "Unable to create dest file, error %d", errcode);
+ return errcode;
+ }
+
+ if( srcfind->nFileSizeLow != 0 ) {
+ hsrc = CreateFileMapping( srcfh, NULL, PAGE_READONLY, 0,
+ srcfind->nFileSizeLow, NULL );
+ if( hsrc == NULL ) {
+ dstfh = INVALID_HANDLE_VALUE;
+ DeleteFile( dst );
+ if( CopyFile( src, dst, TRUE ) == FALSE ) {
+ errcode = GetLastError();
+ sprintf( errorbuf, "Unable to create file mapping, and CopyFile failed, error %d", errcode );
+ return errcode;
+ }
+ nCopyFile++;
+ ulTotalBytesCopied += srcfind->nFileSizeLow;
+ goto DoTime;
+ }
+ if( (psrc = (char *)MapViewOfFile( hsrc, FILE_MAP_READ, 0, 0, 0 )) == NULL){
+ dstfh = INVALID_HANDLE_VALUE;
+ DeleteFile( dst );
+ if( CopyFile( src, dst, TRUE ) == FALSE ) {
+ errcode = GetLastError();
+ sprintf( errorbuf, "Unable to map source file, and CopyFile failed: error %d", errcode );
+ return errcode;
+ }
+ nCopyFile++;
+ ulTotalBytesCopied += srcfind->nFileSizeLow;
+ goto DoTime;
+ }
+ totalbytes = 0;
+ while( !bThreadStop && totalbytes < srcfind->nFileSizeLow && ret == TRUE ) {
+ ret = WriteFile( dstfh,
+ psrc + totalbytes,
+ min( 64*1024, srcfind->nFileSizeLow - totalbytes ),
+ &nBytesWritten, NULL );
+ totalbytes += nBytesWritten;
+ ulTotalBytesCopied += nBytesWritten;
+ }
+ errcode = GetLastError();
+ UnmapViewOfFile( psrc );
+
+ if( bThreadStop == TRUE ) {
+ dstfh = INVALID_HANDLE_VALUE;
+ DeleteFile( dst );
+ *errorbuf = '\0';
+ return errcode;
+ }
+
+ if( ret == FALSE ) {
+ dstfh = INVALID_HANDLE_VALUE;
+ DeleteFile( dst );
+ if( CopyFile( src, dst, TRUE ) == FALSE ) {
+ errcode = GetLastError();
+ sprintf( errorbuf, "%s: CopyFile failed: error %d", dst, errcode);
+ return GetLastError();
+ }
+ nCopyFile++;
+ ulTotalBytesCopied += srcfind->nFileSizeLow;
+ goto DoTime;
+ }
+
+ nMappedCopy++;
+ }
+ }
+
+DoTime:
+ if( dstfh == INVALID_HANDLE_VALUE ) {
+ dstfh = CreateFile( dst, GENERIC_WRITE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
+ 0, NULL );
+ }
+
+ if( dstfh == INVALID_HANDLE_VALUE ) {
+ errcode = GetLastError();
+ DeleteFile( dst );
+ sprintf( errorbuf, "Unable to open destination file to set time, error %d", errcode );
+ return errcode;
+ }
+
+ if( !SetFileTime( dstfh, &srcfind->ftCreationTime,
+ &srcfind->ftLastAccessTime, &srcfind->ftLastWriteTime )) {
+ errcode = GetLastError();
+ sprintf( errorbuf, "Unable to set destination file times, error %d\n", errcode );
+ return errcode;
+ }
+
+ nFcopy++;
+ return 0;
+}
+
+/*
+ * Pick the source having the fewest outstanding operations at the moment.
+ */
+int
+SelectSource()
+{
+ int index = -1;
+ struct SourceList *psl;
+ struct SourceList *opsl;
+ static min;
+
+ EnterCriticalSection( &csSourceList );
+
+ //
+ // Find the first valid source
+ //
+ for( opsl = SourceList; opsl < &SourceList[ MaxSources ]; opsl++ )
+ if( opsl->flags.valid == TRUE )
+ break;
+
+ //
+ // Now locate the source having the fewest pending operations right now
+ //
+ for( psl = opsl+1; psl < &SourceList[ MaxSources ]; psl++ ) {
+ if( psl->flags.valid == TRUE && psl->count < opsl->count )
+ opsl = psl;
+ }
+
+ if( opsl->flags.valid == TRUE ) {
+ opsl->count++;
+ index = opsl - SourceList;
+ }
+
+ LeaveCriticalSection( &csSourceList );
+
+ return index;
+}
+
+/*
+ * We've completed the operation on source 'index'
+ */
+void
+SourceCopyComplete( int index, BOOL fFile )
+{
+ EnterCriticalSection( &csSourceList );
+ SourceList[ index ].count--;
+ if (fFile) SourceList[ index ].ulTotalFiles++;
+ LeaveCriticalSection( &csSourceList );
+}
+
+void
+DisableSource( int index )
+{
+ if( index >= 0 && index < MaxSources ) {
+ EnterCriticalSection( &csSourceList );
+ if( SourceList[ index ].flags.valid == TRUE ) {
+ errormsg( "Disabling %s\n", SourceList[ index ].name );
+ SourceList[ index ].flags.valid = FALSE;
+ }
+ LeaveCriticalSection( &csSourceList );
+ }
+}
+BOOL
+FileTimesEqual( CONST FILETIME *pt1, CONST FILETIME *pt2 )
+{
+ SYSTEMTIME s1, s2;
+
+ if( !FileTimeToSystemTime( pt1, &s1 ) || !FileTimeToSystemTime( pt2, &s2 ) )
+ return FALSE;
+
+ return s1.wHour == s2.wHour &&
+ s1.wMinute == s2.wMinute &&
+ s1.wMonth == s2.wMonth &&
+ s1.wDay == s2.wDay &&
+ s1.wYear == s2.wYear;
+}
+void
+PrintFileTime( char *str, CONST FILETIME *ft )
+{
+ SYSTEMTIME st;
+
+ if( FileTimeToSystemTime( ft, &st ) == FALSE ) {
+ errormsg( "????\n" );
+ return;
+ }
+
+ msg( "%s %u:%u.%u.%u %u/%u/%u\n", str,
+ st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
+ st.wMonth, st.wDay, st.wYear );
+}
+
+DWORD __stdcall
+ThreadWorker( void *arg )
+{
+ struct WorkList *pdl = NULL;
+ int id = (int)arg;
+ HANDLE hdestfind;
+ WIN32_FIND_DATA destfind;
+ int index = 0;
+ char errorbuf[ 100 ];
+ char pathbuf[ MAX_PATH ];
+ DWORD errcode;
+
+ MaxThreads++;
+
+ while( 1 ) {
+ if( pdl != NULL ) {
+ free( pdl->src );
+ free( pdl->dest );
+ free( pdl );
+ pdl = NULL;
+ }
+
+ if( bThreadStop == TRUE )
+ break;
+
+ // Poll for new stuff every 2 seconds. If the thread is set to stop,
+ // then go away.
+ DWORD dwWait;
+ while( 1 )
+ {
+ dwWait = WaitForSingleObject( hWorkAvailSem, 1000 );
+ if( dwWait == WAIT_OBJECT_0 ) {
+ break;
+ }
+
+ if( dwWait == WAIT_TIMEOUT ) {
+ if( bThreadStop == TRUE )
+ break;
+ } else {
+ errormsg( "Thread %d: Semaphore wait failed\n", id );
+ break;
+ }
+ }
+ if ( dwWait != WAIT_OBJECT_0 ) {
+ break;
+ }
+
+ // pick an item off the head of the work list
+ EnterCriticalSection( &csWorkList );
+ pdl = WorkList;
+ if( pdl != NULL ) {
+ WorkList = pdl->next;
+ if (NULL == WorkList) {
+ // just pulled off the tail entry
+ WorkListTail = NULL;
+ }
+ }
+ LeaveCriticalSection( &csWorkList );
+ ReleaseSemaphore( hMaxWorkQueueSem, 1, NULL );
+
+ if( pdl == NULL )
+ break;
+
+ nFilesOnQueue--;
+
+ if( pdl->srcfind.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) {
+ errormsg( "Logic Error: Directory on work list!\n" );
+ continue;
+ }
+
+ pdl->srcfind.dwFileAttributes &= FILE_ATTRIBUTE_MASK;
+
+ hdestfind = FindFirstFile( pdl->dest, &destfind );
+ if( hdestfind != INVALID_HANDLE_VALUE ) {
+ FindClose( hdestfind );
+ destfind.dwFileAttributes &= FILE_ATTRIBUTE_MASK;
+
+ /*
+ * Destination file exists. What should we do?
+ */
+
+ if( oFlag == FALSE ) {
+ /*
+ * We should not overwrite the existing file at the dest
+ */
+ if( vFlag )
+ msg( "%s [SKIP: exists]\n", pdl->dest );
+ ulTotalBytesSkipped += destfind.nFileSizeLow;
+ nSkipped++;
+ continue;
+ }
+
+ if( iFlag && vFlag ) {
+
+ if( destfind.dwFileAttributes !=pdl->srcfind.dwFileAttributes)
+ msg( "%s [ ATTRIBUTES differ ]\n", pdl->dest );
+ if(!FileTimesEqual( &destfind.ftLastWriteTime, &pdl->srcfind.ftLastWriteTime)) {
+ EnterCriticalSection( &csMsg );
+ msg( "%s [ TIMES differ ]\n", pdl->dest );
+ PrintFileTime( "Dest: ", &destfind.ftLastWriteTime );
+ PrintFileTime( "Src: ", &pdl->srcfind.ftLastWriteTime );
+ LeaveCriticalSection( &csMsg );
+ }
+ if( (destfind.nFileSizeHigh != pdl->srcfind.nFileSizeHigh) ||
+ (destfind.nFileSizeLow != pdl->srcfind.nFileSizeLow) )
+ msg( "%s [ SIZES differ ]\n", pdl->dest );
+ }
+
+ if( iFlag &&
+ (destfind.dwFileAttributes == pdl->srcfind.dwFileAttributes) &&
+ FileTimesEqual( &destfind.ftLastWriteTime, &pdl->srcfind.ftLastWriteTime) &&
+ (destfind.nFileSizeHigh == pdl->srcfind.nFileSizeHigh) &&
+ (destfind.nFileSizeLow == pdl->srcfind.nFileSizeLow) ) {
+ if( vFlag )
+ msg("%s [SKIP: same atts, time, size]\n",pdl->dest);
+ ulTotalBytesSkipped += destfind.nFileSizeLow;
+ nSkipped++;
+ continue;
+ }
+
+ if( tFlag &&
+ CompareFileTime( &destfind.ftLastWriteTime, &pdl->srcfind.ftLastWriteTime) >= 0 ) {
+ if( vFlag )
+ msg("%s [SKIP: same or newer time]\n", pdl->dest );
+ ulTotalBytesSkipped += destfind.nFileSizeLow;
+ nSkipped++;
+ continue;
+ }
+
+ if( destfind.dwFileAttributes & FILE_ATTRIBUTE_READONLY )
+ if( rFlag == FALSE && bThreadStop == FALSE ) {
+ if( vFlag )
+ msg( "%s [SKIP: readonly]\n", pdl->dest );
+ ulTotalBytesSkipped += destfind.nFileSizeLow;
+ nSkipped++;
+ continue;
+ }
+
+ /*
+ * Delete the destination file
+ */
+ if( destfind.dwFileAttributes & FILE_ATTRIBUTE_READONLY ) {
+ destfind.dwFileAttributes &= ~FILE_ATTRIBUTE_READONLY;
+ SetFileAttributes( pdl->dest, destfind.dwFileAttributes );
+ }
+
+ if( FFlag == FALSE )
+ (void)DeleteFile( pdl->dest );
+
+ }
+
+ if( FFlag == FALSE ) {
+ while( bThreadStop == FALSE && (index = SelectSource()) >= 0 ) {
+ strcpy( pathbuf, SourceList[index].name );
+ strcat( pathbuf, pdl->src );
+
+ nInProgress++;
+ errcode = fcopy( pathbuf,&pdl->srcfind,pdl->dest,errorbuf);
+ nInProgress--;
+
+ if( errcode == 0 ) {
+ SourceCopyComplete( index, TRUE );
+ msg("%s -> %s [OK]\n", pathbuf, pdl->dest );
+ SetFileAttributes(pdl->dest,pdl->srcfind.dwFileAttributes);
+ break;
+ }
+
+ if( errcode == ERROR_SWAPERROR ) {
+ errormsg( "%s [ SWAP ERROR, will try again... ]\n",pathbuf);
+ Sleep( 5 * 1000 * 60 );
+ continue;
+ }
+ if( bThreadStop == FALSE )
+ errormsg( "%s [FAILED: %s ]\n", pathbuf, errorbuf );
+ if( AFlag == TRUE )
+ break;
+ DisableSource( index );
+ }
+ } else {
+ msg( "%s\n", pdl->dest );
+ }
+
+ if( AFlag == FALSE && index < 0 )
+ errorexit( "%s [FAILED completely]\n", pdl->dest );
+ }
+
+ if( pdl != NULL ) {
+ free( pdl->src );
+ free( pdl->dest );
+ free( pdl );
+ }
+
+ if( MaxThreads-- == 1 ) {
+ if( ExitCode == 0 ) {
+ dwElapsedTime = GetTickCount() - dwElapsedTime;
+ dwElapsedTime /= 1000;
+ BOOL oldqFlag = qFlag;
+ qFlag = FALSE;
+ msg( "%u files copied (%u memory mappped, %u CopyFile )\n",
+ (int)nFcopy, (int)nMappedCopy, (int)nCopyFile );
+ msg( "%u files skipped\n", (int)nSkipped);
+ msg( "%lu bytes in %u seconds: %lu bits/sec\n",
+ (int)ulTotalBytesCopied, dwElapsedTime,
+ dwElapsedTime ? (LONG)(((LONG)ulTotalBytesCopied*8L)/dwElapsedTime) : 0L );
+
+ qFlag = oldqFlag;
+ EnterCriticalSection( &csSourceList );
+ for (int i = 0; i < MaxSources; i++)
+ {
+ if (TRUE == SourceList[i].flags.valid)
+ {
+ msg( "%s %5lu files\n", SourceList[i].name, SourceList[i].ulTotalFiles);
+ }
+ }
+ LeaveCriticalSection( &csSourceList );
+ }
+ SetConsoleTitle( OldConsoleTitle );
+ ExitProcess( ExitCode );
+ }
+
+ ExitThread( ExitCode );
+ return 0;
+}
+
+void
+AddToWorkList( char *src, char *dest, WIN32_FIND_DATA *pfind )
+{
+ struct WorkList *pdl;
+
+ if( WaitForSingleObject( hMaxWorkQueueSem, INFINITE ) != WAIT_OBJECT_0 ) {
+ errormsg( "Semaphore wait failed, can't add to work list\n" );
+ return;
+ }
+
+ if( bThreadStop == TRUE )
+ return;
+
+ if( (pdl = (struct WorkList *)malloc( sizeof( struct WorkList ) ) ) == NULL ){
+ errorexit( "Out of Memory!\n" );
+ return;
+ }
+
+ if( (pdl->dest = _strdup( dest )) == NULL ) {
+ errorexit( "Out of memory!\n" );
+ free( pdl );
+ return;
+ }
+
+ if( (pdl->src = _strdup( src )) == NULL ) {
+ errorexit( "Out of memory!\n" );
+ free( pdl->dest );
+ free( pdl );
+ return;
+ }
+
+ pdl->srcfind = *pfind;
+ pdl->next = NULL;
+
+ EnterCriticalSection( &csWorkList );
+ if (NULL == WorkList) {
+ WorkListTail = WorkList = pdl;
+ } else {
+ WorkListTail->next = pdl; // point the tail to the new entry
+ WorkListTail = pdl; // the new entry becomes the tail
+ }
+ LeaveCriticalSection( &csWorkList );
+
+ nFilesOnQueue++;
+ ReleaseSemaphore( hWorkAvailSem, 1, NULL );
+}
+
+void
+ScanDirectory(
+ char *relpath,
+ char *dest
+ );
+
+void
+ScanDirectoryHelp(
+ char *relpath, // path relative to the source
+ char *dest, // resulting destination directory
+ BOOL fOnlyDirectories, // TRUE if we only want to look for dirs
+ BOOL fOnlyFiles // TRUE if we only want to look for files
+ )
+{
+ int index;
+ int destlen = strlen( dest );
+ int rellen = strlen( relpath );
+ WIN32_FIND_DATA fbuf;
+ HANDLE hfind = INVALID_HANDLE_VALUE;
+ char SourceName[ MAX_PATH ];
+
+ while( 1 ) {
+ if( (index = SelectSource()) < 0 )
+ return;
+ /*
+ * FindFirst/Next is such low overhead on the server that we shouldn't
+ * really count it as a load on the server...
+ */
+ SourceCopyComplete( index, FALSE );
+
+ strcpy( SourceName, SourceList[ index ].name );
+ if( rellen ) {
+ strcat( SourceName, relpath );
+ strcat( SourceName, "\\" );
+ }
+
+ strcat( SourceName,
+ fOnlyDirectories
+ ? "*.*"
+ : (pFlag ? szPattern : "*.*" ) );
+
+ hfind = FindFirstFile( SourceName, &fbuf );
+ if( hfind != INVALID_HANDLE_VALUE )
+ break;
+
+ if (pFlag && ERROR_FILE_NOT_FOUND == GetLastError()) {
+ // simply no files that match the pattern in the directory
+ return;
+ }
+
+ errormsg( "Dir scan of %s failed, error %d [DISABLING]\n", SourceName, GetLastError() );
+ DisableSource( index );
+ }
+
+ do {
+ if( !strcmp( fbuf.cFileName, "." ) || !strcmp( fbuf.cFileName, ".." ) )
+ continue;
+
+ sprintf( &dest[ destlen ], "%s%s",
+ dest[destlen-1] == '\\' ? "" : "\\", fbuf.cFileName );
+
+ if((fbuf.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 ) {
+ //
+ // Not a directory
+ //
+
+ if (fOnlyDirectories) {
+ continue;
+ }
+
+ /*
+ * Queue this file into the work queue!
+ */
+ if( rellen ) {
+ strcpy( SourceName, relpath );
+ strcat( SourceName, "\\" );
+ } else
+ SourceName[0] = '\0';
+
+ strcat( SourceName, fbuf.cFileName );
+ ulTotalBytesScanned += fbuf.nFileSizeLow;
+ AddToWorkList( SourceName, dest, &fbuf );
+ continue;
+ }
+
+ /*
+ * We've found a directory. Descend into it and scan (if we're
+ * not excluding it)
+ */
+
+ if (fOnlyFiles) {
+ continue;
+ }
+
+ if (zFlag) { // no recurse on source: ignore it!
+ continue;
+ }
+
+ for( int i=0; i < MaxDirectoryExcludes; i++ )
+ if( !_stricmp( DirectoryExcludeList[i], fbuf.cFileName ) )
+ break;
+
+ if( i != MaxDirectoryExcludes ) {
+ if( vFlag )
+ msg( "Directory: %s [EXCLUDED]\n", dest );
+ nSkipped++;
+ continue;
+ }
+
+ sprintf( &relpath[ rellen ], "%s%s", rellen ? "\\":"", fbuf.cFileName );
+ if (yFlag) { // no recurse on target: nuke end of dest (the new dir)
+ dest[ destlen ] = '\0';
+ }
+ ScanDirectory( relpath, dest );
+ relpath[ rellen ] = '\0';
+
+ } while( !bThreadStop && FindNextFile( hfind, &fbuf ) == TRUE );
+
+ dest[ destlen ] = '\0';
+
+ FindClose( hfind );
+}
+
+void
+ScanDirectory(
+ char *relpath, // path relative to the source
+ char *dest // resulting destination directory
+ )
+{
+ DWORD dwattrs;
+
+ if( (dwattrs = GetFileAttributes( dest )) == 0xFFFFFFFF ) {
+ msg( "Creating Directory: %s\n", dest );
+ if( FFlag == FALSE && CreateDirectory( dest, NULL ) == FALSE ) {
+ errorexit( "Can not create directory: %s\n", dest );
+ return;
+ }
+
+ } else if( !(dwattrs & FILE_ATTRIBUTE_DIRECTORY) ) {
+ errorexit( "Not a directory: %s\n", dest );
+ return;
+ }
+
+ if (pFlag) {
+ // two passes: one looking for files, one looking for directories
+
+ ScanDirectoryHelp(relpath, dest, FALSE, TRUE);
+ ScanDirectoryHelp(relpath, dest, TRUE, FALSE);
+ } else {
+ ScanDirectoryHelp(relpath, dest, FALSE, FALSE);
+ }
+}
+
+static void
+appendslash( char *p )
+{
+ if( p[ strlen(p) - 1 ] != '\\' )
+ strcat( p, "\\" );
+}
+
+BOOL
+rootpath( char *src, char *dst )
+{
+ char* FilePart;
+ char *p;
+
+ if( src == NULL || *src == '\0' )
+ return FALSE;
+
+ if( GetFullPathName( src, MAX_PATH, dst, &FilePart ) == 0 )
+ return FALSE;
+
+ p = src + strlen(src) - 1;
+ if( *p == '.' )
+ if( p > src ) {
+ p--;
+ if( *p != '.' && *p != ':' && (*p == '\\' || *p == '/') )
+ strcat( dst, "." );
+ }
+
+ return TRUE;
+}
+
+static void
+Usage( char *s )
+{
+ errormsg( "Usage: %s [flags] [-p pattern] [src ...] dest\n", s );
+ errormsg( "Flags:\n" );
+ errormsg( "\t-c|f Get a checked (or free) public tree. Use before -# option\n" );
+ errormsg( "\t-[x86|mips|alpha|ppc] Get release for specific processor family\n" );
+ errormsg( "\t-w Wait until at least 1 'src' is present\n" );
+ errormsg( "\t-# Get NT public release #\n\n" );
+
+ errormsg( "\t-i Skip seemingly identical files (time, attrs, size agree)\n" );
+ errormsg( "\t-l Execute at lower priority\n" );
+ errormsg( "\t-o Do not overwrite any files that are already at dest\n" );
+ errormsg( "\t-p pattern Only files matching the pattern are copied\n" );
+ errormsg( "\t-q Quiet mode\n" );
+ errormsg( "\t-r Overwrite read-only files at dest\n" );
+ errormsg( "\t-t Copy only newer files to dest\n" );
+ errormsg( "\t-v Verbose\n" );
+ errormsg( "\t-y Don't recurse on target\n" );
+ errormsg( "\t-z Don't recurse on source\n" );
+ errormsg( "\t-A Keep going even if there are errors\n" );
+ errormsg( "\t-F Don't actually copy files or create directories\n" );
+ errormsg( "\t~dir Skip any directory named 'dir'\n" );
+ errormsg( "\nIf environment variable FTC_PARANOID is set, then the meaning of -o is\n" );
+ errormsg( "reversed: no -o means don't overwrite, -o means go ahead and overwrite.\n" );
+
+ errormsg( "\nExamples:\n" );
+ errormsg( " Get NT 1338 publics for mips: ftc -mips -1338 dest\n" );
+ errormsg( " Get NT 1338 publics, skip 'alpha' dirs: ftc ~alpha -1338 dest\n" );
+ errormsg( " Wait until release 1338 shares are up: ftc -w -1338 dest\n" );
+ errormsg( " Copy from two sources, no 'obj' dir: ftc ~obj \\\\foo\\dir \\\\bar\\dir dest\n" );
+ ExitProcess( 1 );
+}
+
+BOOL __stdcall
+ControlHandlerRoutine( DWORD dwCtrlType )
+{
+ msg( "Interrupted!\n" );
+ bThreadStop = TRUE;
+ ExitCode = 1;
+ return TRUE;
+}
+
+
+int
+__cdecl
+main(int argc, char *argv[])
+{
+ char *p;
+ SECURITY_ATTRIBUTES sa;
+ int i, argno;
+ DWORD IDThread;
+ char dest[ MAX_PATH ];
+ char relpath[ MAX_PATH ];
+ BOOL cFlag = TRUE; // get a checked NT release? otherwise free
+ BOOL lFlag = FALSE; // low priority?
+ BOOL fParanoid = FALSE; // is FTC_PARANOID set in the environment?
+ SYSTEM_INFO si;
+ CHandle CThread;
+
+ InitializeCriticalSection( &csMsg );
+ InitializeCriticalSection( &csWorkList );
+ InitializeCriticalSection( &csSourceList );
+ ZeroMemory(&si, sizeof( si ));
+ GetConsoleTitle( OldConsoleTitle, sizeof( OldConsoleTitle ) );
+
+ TCHAR szParanoid[100];
+ DWORD len = GetEnvironmentVariable(TEXT("FTC_PARANOID"), szParanoid, ARRAYLEN(szParanoid));
+ if (len > 0)
+ {
+ fParanoid = TRUE;
+ }
+
+ if (fParanoid)
+ {
+ oFlag = FALSE;
+ }
+ else
+ {
+ oFlag = TRUE;
+ }
+
+ for( argno = 1;
+ argno < argc && (argv[argno][0] == '-' || argv[argno][0] == '/' || argv[argno][0] == '~') ;
+ argno++ )
+ {
+ if( argv[argno][0] == '~' )
+ {
+ DirectoryExcludeList[ MaxDirectoryExcludes++ ] = &argv[argno][1];
+
+ }
+ else for( int j=1; argv[argno][j]; j++ )
+ {
+ switch( argv[argno][j] ) {
+ case 'l':
+ lFlag = TRUE;
+ break;
+ case 'q':
+ qFlag = TRUE;
+ break;
+ case 'y':
+ yFlag = TRUE;
+ break;
+ case 'z':
+ zFlag = TRUE;
+ break;
+ case 'w':
+ wFlag = TRUE;
+ break;
+ case 'o':
+ if (fParanoid)
+ {
+ oFlag = TRUE;
+ }
+ else
+ {
+ oFlag = FALSE;
+ }
+ break;
+ case 'F':
+ FFlag = TRUE;
+ break;
+ case 'A':
+ AFlag = TRUE;
+ break;
+ case 'f':
+ cFlag = FALSE;
+ break;
+ case 'c':
+ cFlag = TRUE;
+ break;
+ case 'p':
+ if( j == 1 ) {
+ if ( strcmp( &argv[argno][j], "ppc" ) == 0 ) {
+ si.wProcessorArchitecture = PROCESSOR_ARCHITECTURE_PPC;
+ j += 2;
+ } else if ( strcmp( &argv[argno][j], "p" ) == 0 ) {
+ if (argno + 1 < argc) {
+ pFlag = TRUE;
+ strcpy( szPattern, argv[++argno] );
+ goto nextarg; // go to next argument
+ } else Usage( argv[0] );
+ } else Usage( argv[0] );
+ } else Usage( argv[0] );
+ break;
+ case 'v':
+ vFlag = TRUE;
+ break;
+ case 'r':
+ rFlag = TRUE;
+ break;
+ case 'i':
+ iFlag = TRUE;
+ break;
+ case 't':
+ tFlag = TRUE;
+ break;
+ case 'x':
+ if( j == 1 && strcmp( &argv[argno][j], "x86" ) != 0 )
+ Usage( argv[0] );
+ si.wProcessorArchitecture = PROCESSOR_ARCHITECTURE_INTEL;
+ j += 2;
+ break;
+ case 'm':
+ case 'j':
+ if( j == 1 && strcmp( &argv[argno][1], "jazz" ) != 0 && strcmp(&argv[argno][1], "mips" ) != 0 )
+ Usage( argv[0] );
+ si.wProcessorArchitecture = PROCESSOR_ARCHITECTURE_MIPS;
+ j += 3;
+ break;
+ case 'a':
+ if( j == 1 && strcmp( &argv[argno][j], "alpha" ) != 0 )
+ Usage( argv[0] );
+ si.wProcessorArchitecture = PROCESSOR_ARCHITECTURE_ALPHA;
+ j += 4;
+ break;
+ default:
+ if( isdigit( argv[argno][j] )) {
+ int NTRelNo = atoi( &argv[argno][j] );
+ int MaxNTServers;
+ j += strlen( &argv[argno][j] ) - 1;
+
+ if( si.wProcessorArchitecture == 0 )
+ GetSystemInfo( &si );
+
+ switch( si.wProcessorArchitecture ) {
+ case PROCESSOR_ARCHITECTURE_INTEL:
+ p = "x86";
+ MaxNTServers = 5;
+ break;
+ case PROCESSOR_ARCHITECTURE_MIPS:
+ p = "jazz";
+ MaxNTServers = 2;
+ break;
+ case PROCESSOR_ARCHITECTURE_ALPHA:
+ p = "alpha";
+ MaxNTServers = 2;
+ break;
+ case PROCESSOR_ARCHITECTURE_PPC:
+ p = "ppc";
+ MaxNTServers = 2;
+ break;
+ default:
+ errorexit( "Sorry, don't know server name for your processor type\n" );
+ ExitProcess(1);
+ break;
+ }
+ msg( "Fetch %s public release %03u from %d \\\\nt%s servers...\n",
+ cFlag ? "checked" : "free",
+ NTRelNo, MaxNTServers, p );
+ for( int svr = 1; svr <= MaxNTServers ; svr++ ) {
+ sprintf( dest, "\\\\nt%s%d\\%spub.%03u",
+ p, svr,
+ cFlag ? "chk" : "free",
+ NTRelNo );
+ SourceList[ MaxSources ].name = (char *)malloc(strlen(dest)+2);
+ if( SourceList[ MaxSources ].name == NULL ) {
+ errorexit( "Out of memory!\n" );
+ ExitProcess(1);
+ }
+ strcpy( SourceList[ MaxSources++ ].name, dest );
+ }
+ break;
+ }
+ case '?':
+ Usage( argv[0] );
+ break;
+
+ }
+ }
+
+nextarg:
+ ;
+
+ }
+
+ for( ; argno < argc-1; argno++ ) {
+ if( rootpath( argv[argno], dest ) == FALSE ) {
+ errorexit( "invalid source\n" );
+ ExitProcess(1);
+ }
+ for( p = dest; *p; p++ )
+ if( *p == '/' )
+ *p = '\\';
+ if( (SourceList[ MaxSources ].name = (char *)malloc( strlen( dest ) + 2 )) == NULL ) {
+ errorexit( "Out of memory!\n" );
+ ExitProcess(1);
+ }
+ strcpy( SourceList[ MaxSources++ ].name, dest );
+ }
+
+ if( MaxSources == 0 )
+ Usage( argv[0] );
+
+ for( i=0; i < MaxDirectoryExcludes; i++ )
+ msg( "Exclude Directory: %s\n", DirectoryExcludeList[i] );
+
+ while( 1 ) {
+ char statusbuffer[ MAX_PATH ];
+
+ for( i=0; i < MaxSources; i++ ) {
+ appendslash( SourceList[i].name );
+ if( vFlag == TRUE || wFlag == FALSE )
+ msg( "Validating %s....", SourceList[i].name );
+ sprintf( statusbuffer, "Validating %s", SourceList[i].name );
+ SetConsoleTitle( statusbuffer );
+ if( GetFileAttributes( SourceList[i].name ) == 0xFFFFFFFF ) {
+ if( vFlag == TRUE || wFlag == FALSE )
+ msg( "[DISABLING %s]\n", SourceList[i].name );
+ SourceList[i].flags.valid = FALSE;
+ } else {
+ SourceList[i].flags.valid = TRUE;
+ if( vFlag == TRUE || wFlag == FALSE )
+ msg( "[OK]\n" );
+ }
+ }
+
+ for( i=0; i < MaxSources; i++ )
+ if( SourceList[i].flags.valid == TRUE )
+ break;
+
+ if( i != MaxSources )
+ break;
+
+ if( wFlag == TRUE ) {
+ SetConsoleTitle( "Sleeping awhile..." );
+ Sleep( 3 * 1000 * 60 );
+ } else {
+ SetConsoleTitle( OldConsoleTitle );
+ ExitProcess(1);
+ }
+ }
+
+ SetConsoleTitle( "Sources Present" );
+ LONG cThreads = (MaxSources * 3) + 1;
+
+ /*
+ * hack for ftc -w -678 to exit when the release shares are available
+ */
+ if( argno == argc && wFlag )
+ ExitProcess( 0 );
+
+ if ( argno != argc - 1 ) {
+ Usage( argv[0] );
+ } else if (rootpath (argv[argno], dest) == FALSE ) {
+ errorexit( "Invalid destination\n" );
+ ExitProcess(1);
+ }
+
+ for( p = dest; *p; p++ )
+ if( *p == '/' )
+ *p = '\\';
+
+ for( i=0; i < MaxSources; i++ )
+ if (!strcmp(SourceList[i].name, dest)) {
+ errorexit("Source == dest == %s", SourceList[i].name );
+ ExitThread(1);
+ }
+
+ /*
+ * Create the semaphores for the work lists
+ */
+ sa.nLength = sizeof( sa );
+ sa.lpSecurityDescriptor = NULL;
+ sa.bInheritHandle = TRUE;
+ if( (hWorkAvailSem = CreateSemaphore( &sa, 0, 100000, NULL)) == NULL ) {
+ errorexit( "Unable to create semaphore (err %u)!\n", GetLastError() );
+ ExitProcess(1);
+ }
+
+ hMaxWorkQueueSem = CreateSemaphore( &sa, MAXQUEUE, MAXQUEUE, NULL );
+ if( hMaxWorkQueueSem == NULL ) {
+ errorexit( "Unable to create queue length semaphore (err %u)!\n", GetLastError() );
+ ExitProcess( 1 );
+ }
+
+ /*
+ * Create the thread pool to do the copies
+ */
+ for( i=0; i < cThreads - 1; i++ ) {
+ CThread = CreateThread( (LPSECURITY_ATTRIBUTES)NULL, 0,
+ ThreadWorker, (LPVOID *)i, 0, &IDThread );
+ if( CThread == NULL || CThread == INVALID_HANDLE_VALUE )
+ break;
+ SetThreadPriority( CThread, THREAD_PRIORITY_NORMAL );
+ CThread = INVALID_HANDLE_VALUE;
+ }
+
+ /*
+ * Create the 'update status' thread
+ */
+ CThread = CreateThread( (LPSECURITY_ATTRIBUTES)NULL, 0,
+ StatusWorker,(LPVOID *)0,0,&IDThread );
+ if( CThread != NULL && CThread != INVALID_HANDLE_VALUE ) {
+// SetThreadPriority( CThread, THREAD_PRIORITY_BELOW_NORMAL );
+ CThread = INVALID_HANDLE_VALUE;
+ }
+
+ SetConsoleCtrlHandler( ControlHandlerRoutine, TRUE );
+
+ /*
+ * Produce the directory list
+ */
+ relpath[0] = '\0';
+ dwElapsedTime = GetTickCount();
+
+ SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL );
+ ScanDirectory( relpath, dest );
+
+ if( lFlag )
+ SetPriorityClass( GetCurrentProcess(), IDLE_PRIORITY_CLASS );
+
+ SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_NORMAL );
+
+ // OK, now hWorkAvailSem has a count for every item to copy. But when they
+ // finish copying, each thread waits on this semaphore again. At the end,
+ // everyone will still be waiting! So, add the number of threads to the
+ // count, so each thread notices, one by one, that everything's done.
+ ReleaseSemaphore( hWorkAvailSem, (int)cThreads+1, NULL );
+
+ if( bThreadStop == FALSE )
+ ThreadWorker( 0 );
+
+ return ExitCode;
+}
diff --git a/private/sdktools/ftc/makefile b/private/sdktools/ftc/makefile
new file mode 100644
index 000000000..6ee4f43fa
--- /dev/null
+++ b/private/sdktools/ftc/makefile
@@ -0,0 +1,6 @@
+#
+# DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source
+# file to this component. This file merely indirects to the real make file
+# that is shared by all the components of NT OS/2
+#
+!INCLUDE $(NTMAKEENV)\makefile.def
diff --git a/private/sdktools/ftc/sources b/private/sdktools/ftc/sources
new file mode 100644
index 000000000..7fadaf70f
--- /dev/null
+++ b/private/sdktools/ftc/sources
@@ -0,0 +1,18 @@
+MAJORCOMP= sdktools
+MINORCOMP= ftc
+
+TARGETNAME= ftc
+TARGETTYPE= PROGRAM
+TARGETPATH= obj
+
+UMTYPE= console
+UMENTRY= main
+
+USE_LIBCMT= 1
+
+SOURCES= ftc.cxx app.rc
+
+!IFNDEF MSC_WARNING_LEVEL
+MSC_WARNING_LEVEL=/W3
+!ENDIF
+MSC_WARNING_LEVEL=$(MSC_WARNING_LEVEL) /WX