This tool is waiting around for directory-changes, carrying out specified actions if changes are detectedIt demonstrates the usage of some win32-API-calls and shows how hard it is, to detect
what changes really going on in the filesystem. It mainly uses
FindFirstChangeNotification(),
WaitForSingleObject(), etc. As time of writing,
twapi does not offer a high-level-solution for the script-level, so I coded the program in my second favorite language, PowerBasic [
1].
'==============================================================================
' dirmon.bas v1.20 23.10.2004 (c) HMK 2001-2004, Matthias Hoffmann
' Wartet, bis sich das angegebene Verzeichnis ändert (NICHT-rekursiv);
' gibt dann die Aenderung (ADD:/DEL:/CHG:) sowie die Dateinamen auf STDOUT aus,
' fuehrt wahlweise eine Aktion durch und setzt anschliessend wahlweise die
' Überwachtung fort. Entwickelt aus waitdchg.bas.
' 1.10 22.10.2004: Löschdatei wird automatisch entfernt; Versionsanzeige
' korrigiert; aktuellstes `parsecmd` eingebunden.
' 1.20 23.10.2004: -min, -halt
'==============================================================================
' - Checken: Abbruch, wenn dieses Programm als Pseudo-Service läuft? ggf. echter
' Service!
' - Verschiedene -ffcnflags testen!
' - Win32 FindFirst, FindNext direkt benutzen + Strukturen sofort sichern
' - Abbruchsteuerung - wie? (WaitForMultipleObjects, aber WORAUF NOCH?), ggf.
' auch Taste/Signal etc. checken, immer wenn Wait...() durch Timeout endet
' - RENAME wird jetzt immer als DEL/ADD erkannt... hm...
' - evtl. Erkennungsdatum-/Zeit ausgeben?
' - Weitere konfigurierbare Aktionen und Variablen dort (-cmdxxx)
' - Parameterprüfungen, z.B. ms-Range
' - wahlweise MEHRERE Dirs (posarg1 bis n) überwachbar machen, warten dann mit
' - Paralleles Logging (stdout DUPpen - möglich?)
' - Programm bei Fehler wieder maximieren (dazu exit_prog trappen?->PBDOS)?
' nützt nichts, wenn es DIREKT aufgerufen wurde, da es dann ja sofort endet
' (anders vom CMD.EXE-Prompt).
#tools off
#include "win32api.inc"
'*******************************************************************************
'* Konstanten *
'*******************************************************************************
$Version = $CRLF & "dirmon v1.20 (c) HMK, M.H. 2001-2004" & $CRLF & $CRLF & _
"Ueberwachung eines Ordners auf Aenderungen (nicht rekursiv)." & $CRLF & _
"Anzeige der Aenderungen, wahlweise Aktionen ausfuehren." & $CRLF & $CRLF & _
"Aufruf: dirmon <verzeichnisname> [-schalter]"
'*******************************************************************************
'* Module *
'*******************************************************************************
%parsecmd_static_required = 1
%parseDebug = 0
#include "parsecmd.inc"
'-------------------------------------------------------------------------------
' *** Unterroutinen ***
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
' Verzeichnis NICHT-REKURSIV einlesen in String (kann bei Bedarf mittels
' PARSE in ein Array zerlegt werden) zwecks Vergleich vorher<>nachher
'
function readDir (mask as string) as string
dim tmpBuffer as local string
dim tmpResult as local string
tmpBuffer = dir$(mask, 23)
while len(tmpBuffer)
tmpResult = tmpResult & "," & $DQ & tmpBuffer & $DQ
tmpBuffer = dir$
wend
function = mid$(tmpResult,2)
end function
'-------------------------------------------------------------------------------
' Prüfen, ob Angabe ein DIRECTORY ist; wenn ja, nachfolgenden Backslash ggf.
' entfernen und den Namen nochmals zurückgeben (Leerstring andernfalls);
' zusätzlich / in \ wandeln (falls mit -cmdxxx gearbeitet wird)
'
function testDir (inDir as string) as string
dim t as local string
dim a as local long
a = getattr(inDir)
if isfalse(err) then
if (a and 16) = 16 then
t = rtrim$(inDir,"\")
replace "/" with "\" in t
function = t
end if
end if
end function
'-------------------------------------------------------------------------------
' Zu einer gegebenen Datei die Win32_Find_Data-Struktur einlesen (leider
' bietet PowerBASICs dir$ keinen direkten Zugang hierzu; es sollten also
' eigentlich besser gleich die API-Varianten benutzt werden!), und sich alles
' merken, was zur Erkennung jeglicher Dateiveränderung nötig ist (aber, aus
' Platzgründen, auch nicht mehr!). GRUND: Das Betriebssystem ist doof. Es
' übernimmt ja bereits die Überwachung jedglicher Änderung des Dateisystems,
' teilt einem aber leider nicht mit, WAS GENAU sich nun geändert hat! Also
' muss man die gesamte Erkennung quasi nachbilden; lediglich etwas CPU-Last
' kann man durch FindFirstChangeNotification() sparen, da das System den
' WaitForSingleObject() erst zurückkehren lässt, wenn tatsächlich eine
' Änderung eingetreten ist.
'
function getFileInfo (fileSpec as string) as string
dim fd as local WIN32_FIND_DATA
dim hFile As local long
hFile = FindFirstFile(byval strptr(fileSpec),fd)
if isfalse hFile then
exit function
end if
' Hinweise: Eine Veränderung des DateiINHALTS muss immer eine Fortführung des
' Zeitstempels bewirken! (im Rahmen natürlich der Granularität);
' leider wirken DateiATTRIBUTänderungen NICHT auf den Zeitstempel
' aus!
' OPEN WRITE z.B. aus Tcl bewirkt bereits Zeitstempeländerung ->
' CHG wird gemeldet.
function = mkdwd$(fd.ftLastWriteTime.dwLowDateTime) & _
mkdwd$(fd.ftLastWriteTime.dwHighDateTime) & _
mkdwd$(fd.dwFileAttributes)
' Noch testen: Grössenänderung der Datei durch Abschneiden? Müsste Schreib-
' Zugriff sein, der das Datum aktualisiert!
FindClose hFile
end function
'-------------------------------------------------------------------------------
' *** MAIN ***
'-------------------------------------------------------------------------------
function pbmain()
console name extract$(mid$($Version,3),"(c)")
dim lpPathName as local asciiz*%MAX_PATH ' muss ein DIRECTORY-Name sein!
dim bWatchSubtree as local long
dim hNotify as local long
dim lReturn as local long
dim ffcnflags as local long
dim dirM as local string
dim dirS as local string
dim dir1 as local string
dim atr() as local string
dim dir2 as local string
dim i as local long
dim tmp as local string
dim tmpcmd as local string
dim movdest as local string
ffcnflags = %FILE_NOTIFY_CHANGE_LAST_WRITE or _
%FILE_NOTIFY_CHANGE_FILE_NAME or _
%FILE_NOTIFY_CHANGE_DIR_NAME or _
%FILE_NOTIFY_CHANGE_ATTRIBUTES or _
%FILE_NOTIFY_CHANGE_SIZE or _
%FILE_NOTIFY_CHANGE_CREATION
'-------------------------------------------------------------------------------
' Hilfe initialisieren
'
dim dirName as local long
dim forever as local long
dim delayms as local long
dim timeoms as local long
dim cmd_add as local long
dim cmd_chg as local long
dim ffflags as local long
dim stopfil as local long
dim movedir as local long
dim minimiz as local long
dim haltpgm as local long
dirName = parseRegister(%parsePosArgRequired,"" ,"" ,_
"Name des zu beobachtenden Verzeichnisses, erforderlich!")
forever = parseRegister(%parseTypeFlag ,"loop" ,"" ,_
"Andauernde Ueberwachung (sonst Ende nach erkannten Aenderungen)")
delayms = parseRegister(%parseTypeOption ,"delay" ,"500" ,_
"Wartezeit in ms nach OS-Signal, vor Aenderungs-Erkennung")
timeoms = parseRegister(%parseTypeOption ,"timeout",format$(%INFINITE),_
"Timeout in ms fuer WaitForSingleObject()")
cmd_add = parseRegister(%parseTypeOption ,"cmdadd" ,"" ,_
"Externer Befehl bei ADD; Name ersetzt %~n, vollst.Name %~f")
cmd_chg = parseRegister(%parseTypeOption ,"cmdchg" ,"" ,_
"Externer Befehl bei CHG; Name ersetzt %~n, vollst.Name %~f")
ffflags = parseRegister(%parseTypeOption ,"ffcnflags" ,format$(ffcnflags),_
"Notify-Filter fuer FindFirstChangeNotificaton(), OR-Verkn. aus:" & $CRLF & $TAB & $TAB & _
"%FILE_NOTIFY_CHANGE_FILE_NAME = &H00000001*" & $CRLF & $TAB & $TAB & _
"%FILE_NOTIFY_CHANGE_DIR_NAME = &H00000002*" & $CRLF & $TAB & $TAB & _
"%FILE_NOTIFY_CHANGE_ATTRIBUTES = &H00000004*" & $CRLF & $TAB & $TAB & _
"%FILE_NOTIFY_CHANGE_SIZE = &H00000008*" & $CRLF & $TAB & $TAB & _
"%FILE_NOTIFY_CHANGE_LAST_WRITE = &H00000010*" & $CRLF & $TAB & $TAB & _
"%FILE_NOTIFY_CHANGE_CREATION = &H00000040*" & $CRLF & $TAB & $TAB & _
"%FILE_NOTIFY_CHANGE_SECURITY ? = &H00000100")
stopfil = parseRegister(%parseOptionRequired,"stopfile","$__dirmon.stop",_
"Ende bei Erscheinen dieser Datei im Dir.")
movedir = parseRegister(%parseTypeOption ,"movedir","" ,_
"Ordner; wenn angegeben, (nur) neue Dateien (ADD:) nach dort" & $CRLF & $TAB & $TAB & _
"verschieben. (erfolgt ggf. NACH einem -cmdadd-Exit!)")
minimiz = parseRegister(%parseTypeFlag ,"min" ,"" ,_
"Programm nach dem Starten in Taskbar minimieren")
haltpgm = parseRegister(%parseTypeFlag ,"halt" ,"" ,_
"Laufende Instanz beenden (=stopfile anlegen)")
i = parseParse(command$)
' stdout parseDump
if i < 0 then
if i = %parseHlpRequest or i = %parseErrNoInput then
stdout parseHelp($Version, _
"Sobald eine Aenderung eintritt, wird ein Kuerzel ADD:, CHG: oder DEL: gefolgt" & $CRLF & _
"vom Dateinamen, auf STDOUT ausgegeben (ermoeglicht z.B. das Mitlesen ueber TCLs" & $CRLF & _
"FileEvent-Handler). Via -cmdxyz angegebene Kommandos werden als CMX: geloggt." & $CRLF & _
"Fehlt -loop, endet das Programm nach den ersten Aenderungen mit Errorlevel 1." & $CRLF & _
"Der Abbruch kann durch Strg+Untbr oder Erzeugen der Datei -stopfile im Ordner" & $CRLF & _
"erfolgen (RC0). Mit -delay kann eine geblockte Verarbeitung erreicht werden." & $CRLF & _
"Vorgabe fuer -timeout ist %INFINITE (muss nur geaendert werden bei zeitlich" & $CRLF & _
"begrenzter Ueberwachung). Bei Ende ueber Timeout ist der Errorlevel 2. -movedir" & $CRLF & _
"dient als Ersatz fuer die bisherige Funktion Druckdatenarchivierung (Finanzen)," & $CRLF & _
"es werden dabei die beiden Kuerzel @CP und @DL geloggt. Nachfolger von waitdchg." _
,0)
function = 255
exit function
else
stdout "Fehlerhaftes Token: " & parseLastToken()
stdout "Fehler: " & str$(i)
stdout "Fehlertext: " & parseErrorText(i)
stdout "(Hilfe mit -?)"
function = 255
exit function
end if
end if
movdest = parseArg(movedir)
if len(movdest) then
movdest = testDir(movdest)
if len(movdest) = 0 then
stdout "Verzeichnis -movedir nicht vorhanden: " & parseArg(movedir)
function = 255
exit function
end if
end if
dirM = testDir(parseArg(dirName))
if len(dirM) = 0 then
stdout "Verzeichnis <1> nicht vorhanden: " & parseArg(dirName)
function = 255
exit function
end if
lpPathName = dirM
dirS = dirM & "\"
dirM = dirM & "\*.*"
dir1 = readDir dirM
' v1.20: zusätzliche Flags -min und -stop behandeln
if parseArg(minimiz) = "1" then
showWindow conshndl,%SW_MINIMIZE
end if
if parseArg(haltpgm) = "1" then
open dirS & parseArg(stopfil) for output as #1
close #1
function = err
exit function
end if
do
redim atr(1:parsecount(dir1))
rem Dateiattribute sicherstellen
for i=1 to parsecount(dir1)
atr(i) = getFileInfo(dirS & parse$(dir1,i))
next
bWatchSubtree = %FALSE
hNotify = FindFirstChangeNotification(lpPathName,bWatchSubtree,val(parseArg(ffflags)))
if hNotify = %INVALID_HANDLE_VALUE then
stdout "FindFirstChangeNotification() gescheitert, Rc :=" & str$(hNotify)
function = 64
exit function
end if
function = 0 ' Strg+Untbr?
lReturn = WaitForSingleObject(hNotify, val(parseArg(timeoms)))
if lReturn = 0 then
function = 1 ' Irgendeine Änderung ist eingetreten
elseif lReturn = 258 then ' Timeout
function = 2
end if
rem stdout "rc(WaitForSingleObject) :=" & str$(lReturn) ' 0, timeout = 258
lReturn = FindCloseChangeNotification(hNotify)
' Jetzt Chance für Tastendruck/anderweitigen Abbruchcheck
' gibt dem OS Zeit, die Änderungen zu reflektieren (vermutlich
' zu wenig!) - verhindert evtl. Doppelterkennungen bei REN/DEL
sleep val(parseArg(delayms))
dir2 = readDir dirM
' Herausfinden, welche Änderungen aufgetreten sind
for i=1 to parsecount(dir1)
tmp = parse$(dir1,i)
if instr(dir2,tmp) = 0 then
stdout "DEL: " & tmp
elseif atr(i) <> getFileInfo(dirS & tmp) then
' Bugfix v1.2: bei Verwendung ohne -loop wurde Stopfile (tw.?) als CHG: erkannt
if tmp = parseArg(stopfil) then
' v1.10: stopfil sofort wieder löschen
setattr dirS & tmp,0
kill dirS & tmp
function = 0
exit function
end if
' >>> Problem: getFileInfo reflektiert evtl. schon wieder einen SPÄTEREN
' Zustand als der readDir() weiter oben; das darf nicht sein! ===>
' Win32-FindFirst/FindNext muss benutzt werden mit SOFORTIGER Sicherstellung
' der relevanten Attribute!
stdout "CHG: " & tmp
tmpcmd = parseArg(cmd_chg)
if len(tmpcmd) then
replace "%~f" with dirS & tmp in tmpcmd
replace "%~n" with tmp in tmpcmd
shell tmpcmd
stdout "CMC: " & tmpcmd & " (ready)"
end if
end if
next
for i=1 to parsecount(dir2)
tmp = parse$(dir2,i)
if instr(dir1,tmp) = 0 then
if tmp = parseArg(stopfil) then
' v1.10: stopfil sofort wieder löschen
setattr dirS & tmp,0
kill dirS & tmp
function = 0
exit function
end if
stdout "ADD: " & tmp
tmpcmd = parseArg(cmd_add)
if len(tmpcmd) then
replace "%~f" with dirS & tmp in tmpcmd
replace "%~n" with tmp in tmpcmd
shell tmpcmd
stdout "CMA: " & tmpcmd & " (ready)"
end if
if len(movdest) then
if len(dir$(dirS & tmp, 23)) then
' Abfrage, falls Datei bereits von -cmdadd gelöscht
filecopy dirS & tmp, movdest & "\" & tmp
stdout "@CP: " & tmp & " -> " & movdest & " (" & format$(Errclear) & ")"
setattr dirS & tmp,0
kill dirS & tmp
stdout "@DL: " & tmp & " (" & format$(Errclear) & ")"
end if
end if
end if
next
dir1 = dir2
if parseArg(forever) = "0" then
exit loop
end if
loop
end function