Jump to content

Script with _WinAPI_ReadDirectoryChanges


Go to solution Solved by Nine,

Recommended Posts

Hi,

I am creating a script to monitor the changes happen in a given directory/folder. Found _WinAPI_ReadDirectoryChanges which demonstrate an accurate detection on the change (I am detecting the change of size), however, I need some help here.

Basically my script does as following:

  1. use it to monitor changes for a given directory
  2. as soon as there is changes occur, it start and continue to monitor (keep looping)
  3. as soon as there is no changes detect for more than 5 second, it exit the loop
#include <APIFilesConstants.au3>
#include <Array.au3>
#include <AutoItConstants.au3>
#include <MsgBoxConstants.au3>
#include <WinAPIError.au3>
#include <WinAPIFiles.au3>
#include <WinAPIMem.au3>

Global $g_sPath = "C:\Desktop\TestProgram_SIM\"
Local $hDirectory = _WinAPI_CreateFileEx($g_sPath, $OPEN_EXISTING, $FILE_LIST_DIRECTORY, BitOR($FILE_SHARE_READ, $FILE_SHARE_WRITE), $FILE_FLAG_BACKUP_SEMANTICS)

If @error Then
    _WinAPI_ShowLastError('', 1)
EndIf

Local $pBuffer = _WinAPI_CreateBuffer(8388608)

Local $aData

ConsoleWrite(TimerDiff($iStartTime) & @CRLF)
While TimerDiff($iStartTime) < 3000
        ConsoleWrite("IN" & @CRLF)
        $aData = _WinAPI_ReadDirectoryChanges($hDirectory, $FILE_NOTIFY_CHANGE_SIZE, $pBuffer, 8388608, 1)
        If Not @error Then
;~           _ArrayDisplay($aData, '_WinAPI_ReadDirectoryChanges')
            ConsoleWrite("Changes Occur @Timer:" & TimerDiff($iStartTime) & @CRLF)
        Else
            ConsoleWrite("NO detection:" & @CRLF)
            _WinAPI_ShowLastError('', 1)
        EndIf
        Sleep(100)
        
WEnd
ConsoleWrite("OUT" & @CRLF)

I am using the Example found in Helpfile, but couldn't workaround how to exit the loop after no more changes is detected, the script will just pause and wait for any changes (continuosly) after calling _WinAPI_ReadDirectoryChanges.

Appreciate if anyone could shade some light here, thank you.

Link to comment
Share on other sites

..for that example, you'll need to create a file in the path you're watching and use that file to trigger an exit. Say "thatSit.txt". When _WinAPI_ReadDirectoryChanges() catches that, you know that that is your cue to exit. Otherwise you'll need to ProcessClose it.
Time for me to catch some ZZzzz :)

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting.
autoit_scripter_blue_userbar.png

Link to comment
Share on other sites

  • Solution
Posted (edited)

As per help file :

Quote

The _WinAPI_ReadDirectoryChanges() function works only in synchronous mode.

It means this is a blocking function, it will wait forever for a change.

But there is a way to make it asynchronous.  You would need to use the Overlapped version of it.  Search the forum, I believe there is an example of it.

ps.  made one for the fun of it :

#include <Constants.au3>
#include <WinAPI.au3>

Opt("MustDeclareVars", True)

HotKeySet("{ESC}", Terminate)

Global Const $WATCH_PATH = "C:\Apps\Temp"
Global Const $BUFFER_LENGTH = 4096
Global Const $NOTIFY_FLAGS = BitOR($FILE_NOTIFY_CHANGE_FILE_NAME, $FILE_NOTIFY_CHANGE_DIR_NAME)

Global $tOverlapped, $hDirectory, $hCompletion, $pCompletion, $pDataBuffer, $hEvent

DirectoryChange()

Func DirectoryChange()
  $tOverlapped = DllStructCreate($tagOVERLAPPED)
  $hEvent = _WinAPI_CreateEvent(0, False, False)
  $tOverlapped.hEvent = $hEvent

  $hDirectory = _WinAPI_CreateFileEx($WATCH_PATH, $OPEN_EXISTING, $FILE_LIST_DIRECTORY, _
      BitOR($FILE_SHARE_READ, $FILE_SHARE_WRITE, $FILE_SHARE_DELETE), _
      BitOR($FILE_FLAG_BACKUP_SEMANTICS, $FILE_FLAG_OVERLAPPED))

  $hCompletion = DllCallbackRegister(CompletionRoutine, "none", "dword;dword;ptr")
  $pCompletion = DllCallbackGetPtr($hCompletion)
  $pDataBuffer = _WinAPI_CreateBuffer($BUFFER_LENGTH)

  _WinAPI_ReadDirectoryChangesW($hDirectory, $NOTIFY_FLAGS, $pDataBuffer, $BUFFER_LENGTH, True, $tOverlapped, $pCompletion)
  OnAutoItExitRegister(CleanUp)

  While True
    _WinAPI_WaitForSingleObjectEx($hEvent, 0, 1)
    Sleep(100)
  WEnd
EndFunc   ;==>DirectoryChange

;~ Make decisions on file changes in here
Func FileNotifyUser($nFileAction, $sFileName)
  Switch $nFileAction
    Case $FILE_ACTION_ADDED
      ConsoleWrite("File added: " & $sFileName & @CRLF)
    Case $FILE_ACTION_REMOVED
      ConsoleWrite("File removed: " & $sFileName & @CRLF)
    Case $FILE_ACTION_RENAMED_OLD_NAME
      ConsoleWrite("File renamed from: " & $sFileName & @CRLF)
    Case $FILE_ACTION_RENAMED_NEW_NAME
      ConsoleWrite("File renamed to: " & $sFileName & @CRLF)
  EndSwitch
EndFunc   ;==>FileNotifyUser

Func CleanUp()
  _WinAPI_CancelIoEx($hDirectory, $tOverlapped)
  _WinAPI_CloseHandle($hDirectory)
  _WinAPI_FreeMemory($pDataBuffer)
  _WinAPI_CloseHandle($hEvent)
  DllCallbackFree($hCompletion)
EndFunc   ;==>CleanUp

Func CompletionRoutine($nError, $nTransfered, $pOverlapped)
  Local $pBuffer = $pDataBuffer, $tNotify, $nLength

  While True
    $nLength = DllStructGetData(DllStructCreate("dword", $pBuffer + 8), 1)
    $tNotify = DllStructCreate("dword NextOffset;dword Action;dword NameLength;wchar FileName[" & $nLength / 2 & "]", $pBuffer)

    FileNotifyUser($tNotify.Action, $tNotify.FileName)

    If Not $tNotify.NextOffset Then ExitLoop
    $pBuffer += $tNotify.NextOffset
  WEnd

  _WinAPI_ReadDirectoryChangesW($hDirectory, $NOTIFY_FLAGS, $pDataBuffer, $BUFFER_LENGTH, 0, $tOverlapped, $pCompletion)
EndFunc   ;==>CompletionRoutine

Func _WinAPI_ReadDirectoryChangesW($hDirectory, $iFilter, $pBuffer, $iLength, $bSubtree = 0, $tOverlapped = 0, $pCompletion = 0)
  Local $aRet = DllCall('kernel32.dll', 'bool', 'ReadDirectoryChangesW', 'handle', $hDirectory, 'struct*', $pBuffer, _
      'dword', $iLength - Mod($iLength, 4), 'bool', $bSubtree, 'dword', $iFilter, 'dword*', 0, 'struct*', $tOverlapped, 'PTR', $pCompletion)
  If @error Or Not $aRet[0] Then Return SetError(@error + 10, @extended, 0)
  Return SetExtended(_WinAPI_GetLastError(), $aRet[0])
EndFunc   ;==>_WinAPI_ReadDirectoryChangesW

Func _WinAPI_CancelIoEx($hFile, $tOverlapped)
  Local $aRet = DllCall('kernel32.dll', 'bool', 'CancelIoEx', 'handle', $hFile, 'struct*', $tOverlapped)
  If @error Then Return SetError(@error, @extended, 0)
  Return $aRet[0]
EndFunc   ;==>_WinAPI_CancelIoEx

Func _WinAPI_WaitForSingleObjectEx($hEvent, $nMilliseconds, $bAlertable = 0)
  Local $aRet = DllCall('kernel32.dll', 'dword', 'WaitForSingleObjectEx', 'handle', $hEvent, 'dword', $nMilliseconds, 'bool', $bAlertable)
  If @error Then Return SetError(@error, @extended, 0)
  Return $aRet[0]
EndFunc   ;==>_WinAPI_WaitForSingleObjectEx

Func Terminate()
  Exit
EndFunc   ;==>Terminate

 

Edited by Nine
made example script
Link to comment
Share on other sites

Beautiful @Nine.
Did change the buffer size from 4k to 64k as otherwise creating and removing 200 files at once would not catch them all. Any reason to the set a small buffer on this version  ?

My  counter:

... ...
Func FileNotifyUser($nFileAction, $sFileName)
    Local Static $iCount = 0
    Switch $nFileAction
        Case $FILE_ACTION_ADDED
            $iCount += 1
            ConsoleWrite($iCount & @TAB & "File added: " & $sFileName & @CRLF)
        Case $FILE_ACTION_REMOVED
            $iCount -= 1
            
            ... ...
Edited by argumentum
more

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting.
autoit_scripter_blue_userbar.png

Link to comment
Share on other sites

Appreciate it very much @Nine Your fun save my day : )

I added some stuff to made detect size change and exit if no activity for more than 5 seconds.

Global Const $NOTIFY_FLAGS = BitOR($FILE_NOTIFY_CHANGE_FILE_NAME, $FILE_NOTIFY_CHANGE_DIR_NAME, $FILE_NOTIFY_CHANGE_SIZE)

 

While True
    _WinAPI_WaitForSingleObjectEx($hEvent, 100, 1) ; Wait for 100ms

    ; Check if 5 seconds have passed since the last activity
    If TimerDiff($iLastActivityTime) > 5000 Then
        ConsoleWrite("No activity for 5 seconds. Exiting..." & @CRLF)
        ExitLoop
    EndIf
WEnd

 

Func FileNotifyUser($nFileAction, $sFileName)
    $iLastActivityTime = TimerInit() ; Reset the activity timer
    
    ;remain unchanged

Thank you all for the help! 

Link to comment
Share on other sites

@hudsonhock  Few suggestions.

1- By specifying a timeout on _WinAPI_WaitForSingleObjectEx, you disable the ability to use the HotKeySet (or any other means to stop the script)

2- You could use result of _WinAPI_WaitForSingleObjectEx to determine if an event occurred or not, would make the script more readable.

Local $iRes, $hTimer = TimerInit()
  While TimerDiff($hTimer) < 5 * 60 * 1000 ; 5 mins
    $iRes = _WinAPI_WaitForSingleObjectEx($hEvent, 0, 1)
    If $iRes = $WAIT_IO_COMPLETION Then $hTimer = TimerInit() ; $WAIT_IO_COMPLETION = 0xC0
    Sleep(100)
  WEnd
  ConsoleWrite("No activity for 5 minutes. Exiting..." & @CRLF)
Edited by Nine
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...