Jump to content

Folder watcher doesn't work when folder has more than a few files


Recommended Posts

10 minutes ago, emendelson said:

Does this mean that I'll need two executables, or

Same executable script. Run one with no command line, then the same with a $CmdLineRaw that has a string like "/watchThis:C\myPath"

MyCmdLineParser()
Func MyCmdLineParser()
    For $n = 1 To $CmdLine[0]
        If StringInStr($CmdLine[$n], "/watchThis:") Then
            _TheFuncThatActuallyWatch(StringTrimLeft($CmdLine[$n], StringLen("/watchThis:")))
            Exit
        EndIf
    Next
EndFunc
ShellExecute(@ScriptFullPath, '"/watchThis:C\myPath"')

 

Edited by argumentum
better code

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

Link to comment
Share on other sites

..ok

#AutoIt3Wrapper_AU3Check_Parameters=-q -d -w 1 -w 2 -w 3 -w 4 -w 5 -w 6 -w 7
#Au3Stripper_Ignore_Funcs=_Forked_*
#include <Fork.au3>
#include <MailSlot.au3>; http://www.autoitscript.com/forum/index.php?showtopic=106710

;~  ; https://docs.microsoft.com/en-us/windows/desktop/api/winnt/ns-winnt-_file_notify_information
;~  Global Const $FILE_ACTION_ADDED = 0x00000001 ; The file was added to the directory.
;~  Global Const $FILE_ACTION_REMOVED = 0x00000002 ; The file was removed from the directory.
;~  Global Const $FILE_ACTION_MODIFIED = 0x00000003 ; The file was modified. This can be a change in the time stamp or attributes.
;~  Global Const $FILE_ACTION_RENAMED_OLD_NAME = 0x00000004 ; The file was renamed and this is the old name.
;~  Global Const $FILE_ACTION_RENAMED_NEW_NAME = 0x00000005 ; The file was renamed and this is the new name.


Global $emendelson_watchPath = @TempDir & "\~TEST~"

Global $pidFileMon = 0, $hMailSlot, $sMailSlotName = "\\.\mailslot\FFMonitorNoGUI"

Global $aFILE_ACTION = StringSplit("ADDED,REMOVED,MODIFIED,OLD_NAME,NEW_NAME", ",", 0)
$aFILE_ACTION[0] = "fork msg."

_Fork_AutoItWinTitlePrefix("FileMonitorNoGUI")
_Fork_Startup()
Main()


Func Main()
    $hMailSlot = _MailSlotCreate($sMailSlotName)
    If @error Then
        MsgBox(48 + 262144, "MailSlot", "Failed to create new account!" & @CRLF & "Probably one using that 'address' already exists.")
        Exit 6
    EndIf
    OnAutoItExitRegister("GBye")
    $pidFileMon = _Fork_Func('_Forked_ReadDirectoryChanges("' & $emendelson_watchPath & '")', Default, '/ErrorStdOut')
    Local $aFileEvents, $iMessageCount
    While 1
        If _MailSlotGetMessageCount($hMailSlot) Then
            $aFileEvents = ReturnArray($hMailSlot)
            If @error Then
                _DebugArrayDisplay($aFileEvents) ; error string is in $aFileEvents[0][1]
                ContinueLoop
            EndIf


            _DebugArrayDisplay($aFileEvents) ; here you do your thing ;)


            ContinueLoop
        EndIf
        Sleep(1000) ; $nMsg = GUIGetMsg()
    WEnd
EndFunc   ;==>Main

Func ReturnArray($hMailSlot)
    Local Static $iNumberOfMessagesOverall = 0
    Local $i = 0, $aData, $sData, $iSize = _MailSlotCheckForNextMessage($hMailSlot)
    If $iSize Then
        $sData = _MailSlotRead($hMailSlot, $iSize, 1)
        $iNumberOfMessagesOverall += 1
    EndIf
    If StringInStr($sData, '****') Then
        Local $aTmp[1][4]
        $aTmp[0][0] = 0
        $aTmp[0][1] = $sData
        Return SetError(1, 0, $aTmp) ; and know that there was an error to do something about it - not in this example, but you in your code ;)
    EndIf
    $aData = StringSplit($sData, '*', 0)
    Local $aTmp[UBound($aData) + 1][4]
    For $n = 1 To UBound($aData) - 3 Step 3
        $i += 1
        If $n > 1 Then
            $aTmp[$i][0] = $iNumberOfMessagesOverall & ' (' & ($n + 2) / 3 & ')' ; to show that there were
        Else ;                                                                    more than one file action
            $aTmp[$i][0] = $iNumberOfMessagesOverall ;                             reported on that message.
        EndIf
        $aTmp[$i][1] = $aData[$n]
        $aTmp[$i][2] = $aFILE_ACTION[Int($aData[$n + 2])]
        $aTmp[$i][3] = $aData[$n + 1]
        If $aTmp[$i][2] = $aFILE_ACTION[0] Then $aTmp[0][3] &= '  -  ( timestamp NOW is ' & @HOUR & ':' & @MIN & ':' & @SEC & '.' & @MSEC & ' )'
    Next
    ReDim $aTmp[$i + 1][4]
    $aTmp[0][0] = $i
    Return $aTmp
EndFunc   ;==>ReturnArray

Func GBye()
    If $pidFileMon Then ProcessClose($pidFileMon)
EndFunc   ;==>GBye


#Region _WinAPI_ReadDirectoryChanges fork

Func _Forked_ReadDirectoryChanges($sPath) ; based on the _WinAPI_ReadDirectoryChanges() example in the help file
    TraySetState(2) ;  $TRAY_ICONSTATE_HIDE (2) = Destroys/Hides the tray icon

    DirCreate($sPath)
    If Not FileExists($sPath) Then
        _MailSlotWrite($sMailSlotName, 'FileExists' & @CRLF & '@error = ' & @error & @CRLF & '@extended = ' & @extended & @CRLF & 'LastError = ' & 'Unable to create / find folder.' & '****')
        Exit 3
    EndIf

    Local $sData, $hDirectory = _WinAPI_CreateFileEx($sPath, $OPEN_EXISTING, $FILE_LIST_DIRECTORY, BitOR($FILE_SHARE_READ, $FILE_SHARE_WRITE), $FILE_FLAG_BACKUP_SEMANTICS)
    If @error Then
        $sData = _WinAPI_GetLastError()
        _MailSlotWrite($sMailSlotName, '_WinAPI_CreateFileEx' & @CRLF & '@error = ' & @error & @CRLF & '@extended = ' & @extended & @CRLF & 'LastError = ' & $sData & '****')
        Exit 4
    EndIf

    Local $pBuffer = _WinAPI_CreateBuffer(8388608)

    While 1
        $sData = __WinAPI_ReadDirectoryChanges_RetStr($hDirectory, BitOR($FILE_NOTIFY_CHANGE_FILE_NAME, $FILE_NOTIFY_CHANGE_DIR_NAME, $FILE_NOTIFY_CHANGE_LAST_WRITE), $pBuffer, 8388608, 1)
        If Not @error Then
            _MailSlotWrite($sMailSlotName, $sData)
        Else
            $sData = _WinAPI_GetLastError()
            _MailSlotWrite($sMailSlotName, 'ReadDirectoryChanges' & @CRLF & '@error = ' & @error & @CRLF & '@extended = ' & @extended & @CRLF & 'LastError = ' & $sData & '****')
            Exit 5
        EndIf
    WEnd
EndFunc   ;==>_Forked_ReadDirectoryChanges

Func __WinAPI_ReadDirectoryChanges_RetStr($hDirectory, $iFilter, $pBuffer, $iLength, $bSubtree = 0) ; a _WinAPI_ReadDirectoryChanges mod. to return a string and save time, on this script.
    Local $aRet = DllCall('kernel32.dll', 'bool', 'ReadDirectoryChangesW', 'handle', $hDirectory, 'struct*', $pBuffer, _
            'dword', $iLength - Mod($iLength, 4), 'bool', $bSubtree, 'dword', $iFilter, 'dword*', 0, 'ptr', 0, 'ptr', 0)
    If @error Or Not $aRet[0] Or (Not $aRet[6]) Then Return SetError(@error + 10, @extended, 0)

    $pBuffer = $aRet[2] ; if updated by the DllCall in case of not word align

    Local $tFNI, $iBuffer = 0, $iOffset = 0, $sData = "", $timestamp = @HOUR & ':' & @MIN & ':' & @SEC & '.' & @MSEC

    Do
        $iBuffer += $iOffset
        $tFNI = DllStructCreate('dword NextEntryOffset;dword Action;dword FileNameLength;wchar FileName[' & (DllStructGetData(DllStructCreate('dword FileNameLength', $pBuffer + $iBuffer + 8), 1) / 2) & ']', $pBuffer + $iBuffer)
        $sData &= $timestamp & '*' & DllStructGetData($tFNI, "FileName") & '*' & DllStructGetData($tFNI, "Action") & '*'
        $iOffset = DllStructGetData($tFNI, "NextEntryOffset") ; the "timestamp" costs time to generate, consequently not precise,
    Until Not $iOffset ;                                         but is more accurate here, than on the the other side of the IPC.

    Return $sData
EndFunc   ;==>__WinAPI_ReadDirectoryChanges_RetStr

#EndRegion _WinAPI_ReadDirectoryChanges fork

this is a good head start ;) 

Edited by argumentum
better code

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

Just for the fun of it, a creative alternative :

#include <APIFilesConstants.au3>
#include <Array.au3>
#include <MsgBoxConstants.au3>
#include <WinAPIError.au3>
#include <WinAPIFiles.au3>
#include <WinAPIMem.au3>

HotKeySet ("^!{END}", _Exit) ; in case of emergency ;-)

Global $g_sPath = "C:\Apps\Temp\", $nBufferLen = 1048576
If Not FileExists($g_sPath) Then Exit MsgBox($MB_SYSTEMMODAL, 'Error', 'Unable to access folder')

Local $hDirectory = _WinAPI_CreateFileEx($g_sPath, $OPEN_EXISTING, $GENERIC_READ+$GENERIC_WRITE, $FILE_SHARE_READ+$FILE_SHARE_WRITE, $FILE_FLAG_BACKUP_SEMANTICS)
If @error Then Exit MsgBox($MB_SYSTEMMODAL, 'Error', 'Unable to get handle')

TraySetToolTip ("Watching for folder " & $g_sPath & @LF & "Press Esc to Exit")
_CreateChild ()

Local $pBuffer = _WinAPI_CreateBuffer($nBufferLen), $aData, $sFileName
While True
    $aData = _WinAPI_ReadDirectoryChanges($hDirectory, _
    $FILE_NOTIFY_CHANGE_FILE_NAME + _
    $FILE_NOTIFY_CHANGE_DIR_NAME + _
    $FILE_NOTIFY_CHANGE_ATTRIBUTES + _
    $FILE_NOTIFY_CHANGE_SIZE + _
    $FILE_NOTIFY_CHANGE_LAST_WRITE + _
    $FILE_NOTIFY_CHANGE_LAST_ACCESS + _
    $FILE_NOTIFY_CHANGE_CREATION + _
    $FILE_NOTIFY_CHANGE_SECURITY, $pBuffer, $nBufferLen, 1) ; remove unnecessary notifications
    If Not @error Then
    For $i = 1 To $aData[0][0]
      $sFileName = $g_sPath & $aData[$i][0]
      If FileExists ($sFileName) Then
        ;_PrintThatFile ($sFileName)
        FileDelete ($sFileName)
      EndIf
    Next
    Else
        _WinAPI_ShowLastError('', 1)
    EndIf
WEnd

Func _Exit ()
  Exit
EndFunc

Func _CreateChild ()
  Local $sTempFile = _WinAPI_GetTempFileName (@TempDir, "~")
  ;MsgBox ($MB_SYSTEMMODAL,"",$sTempFile)
  Local $sScript = "#NoTrayIcon" & @CRLF & _
    "HotKeySet ('{ESC}', _Exit)" & @CRLF & _
    "While 1" & @CRLF & _
    "Sleep (100)" & @CRLF & _
    "WEnd" & @CRLF & _
    "Func _Exit ()" & @CRLF & _
    "RunWait('TaskKill /PID ' & $CmdLine[1] & ' /F', '', @SW_HIDE)" & @CRLF & _
    "FileDelete (@ScriptFullPath)" & @CRLF & _
    "Exit" & @CRLF & _
    "EndFunc"
  FileWrite ($sTempFile, $sScript)
  Run (@AutoItExe & " " & $sTempFile & " " & @AutoItPID, @TempDir, @SW_HIDE)
EndFunc

:)

Edited by Nine
Link to comment
Share on other sites

This has all been extremely helpful, and I apologize for asking for more help.

@Nine- your script is absolutely perfect for the kind of monitoring I'm doing, but the blocking function means that I can't get a Tray icon that will let the user exit the script without using the task manager. I'm creating this for distribution, so I don't want to register a hotkey on someone else's system.

@argumentum - I used your technique of launching the script again, after setting the Tray icon near the start of the script, but exactly the same thing happened that happens when I use Nine's method - the Tray icon doesn't let me exit or display information. I tried putting the Tray icon inside the function that watches files, but the only effect of that was to make the tray icon menu pop up when I wrote a file into the watched folder.

I've been experimenting with this for a few hours, with no success. Can anyone who knows more than I do suggest a way to keep the tray icon functioning while watching a folder? Argumentum, I tried adding a tray icon to your sample file monitor script, but I couldn't get it to display, so I'm not certain whether the technique in that script will help either. But of course I'm only stumbling around in the code, without knowing how to use it.

Edited by emendelson
Link to comment
Share on other sites

...my insistence is due to the fact that you are now facing: can not even close it from the tray or hotkey or anything.
I've started with "all in the loop" just like most ppl. but I needed more. Then "on event" and AdLib .... but I needed more.
So more was available, if I could just wrap my head around it. It takes time but, you too can wrap your head around it :) 

The code I gave you is the closest without taking you out of the ditch and doing it all my self. That is not the idea.
Where in your code $sFileName = FileFindNextFile($hSearch) is at, the array in my code has, so, is fairly obvious how to merge both codes.

Take your time. You can do it. :thumbsup:
 

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

@emendelson Ok then you could have used  the child tray instead of the parent tray.  It only requires a few lines change.  Anyway I made the changes for myself, it is better that way.  This is my last shot on it :

#NoTrayIcon
#include <APIFilesConstants.au3>
#include <Array.au3>
#include <MsgBoxConstants.au3>
#include <WinAPIError.au3>
#include <WinAPIFiles.au3>
#include <WinAPIMem.au3>

Global $g_sPath = "C:\Apps\Temp\", $nBufferLen = 1048576
If Not FileExists($g_sPath) Then Exit MsgBox($MB_SYSTEMMODAL, 'Error', 'Unable to access folder')

Local $hDirectory = _WinAPI_CreateFileEx($g_sPath, $OPEN_EXISTING, $GENERIC_READ+$GENERIC_WRITE, $FILE_SHARE_READ+$FILE_SHARE_WRITE, $FILE_FLAG_BACKUP_SEMANTICS)
If @error Then Exit MsgBox($MB_SYSTEMMODAL, 'Error', 'Unable to get handle')

_CreateChild ()

Local $pBuffer = _WinAPI_CreateBuffer($nBufferLen), $aData, $sFileName
While True
    $aData = _WinAPI_ReadDirectoryChanges($hDirectory, _
    $FILE_NOTIFY_CHANGE_FILE_NAME + _
    $FILE_NOTIFY_CHANGE_DIR_NAME + _
    $FILE_NOTIFY_CHANGE_ATTRIBUTES + _
    $FILE_NOTIFY_CHANGE_SIZE + _
    $FILE_NOTIFY_CHANGE_LAST_WRITE + _
    $FILE_NOTIFY_CHANGE_LAST_ACCESS + _
    $FILE_NOTIFY_CHANGE_CREATION + _
    $FILE_NOTIFY_CHANGE_SECURITY, $pBuffer, $nBufferLen, 1) ; remove unnecessary notifications
    If Not @error Then
    For $i = 1 To $aData[0][0]
      $sFileName = $g_sPath & $aData[$i][0]
      If FileExists ($sFileName) Then
        ;_PrintThatFile ($sFileName)
        FileDelete ($sFileName)
      EndIf
    Next
    Else
        _WinAPI_ShowLastError('', 1)
    EndIf
WEnd

Func _CreateChild ()
  Local $sTempFile = _WinAPI_GetTempFileName (@TempDir, "~")
  Local $sScript = _
    "OnAutoItExitRegister (_Exit)" & @CRLF & _
    "TraySetToolTip ('Monitorying folder ' & $CmdLine[2])" & @CRLF & _
    "Opt ('TrayAutoPause', 0)" & @CRLF & _
    "While 1" & @CRLF & _
    "Sleep (100)" & @CRLF & _
    "WEnd" & @CRLF & _
    "Func _Exit ()" & @CRLF & _
    "RunWait('TaskKill /PID ' & $CmdLine[1] & ' /F', '', @SW_HIDE)" & @CRLF & _
    "FileDelete (@ScriptFullPath)" & @CRLF & _
    "Exit" & @CRLF & _
    "EndFunc"
  FileWrite ($sTempFile, $sScript)
  Run (@AutoItExe & " " & $sTempFile & " " & @AutoItPID & ' "' & $g_sPath & '"', @TempDir, @SW_HIDE)
EndFunc

 

 

 

Edited by Nine
Link to comment
Share on other sites

I had some time and threw together a non-blocking version of ReadDirectoryChangesW.  More watch flags can be added if you need other notifications (see here).  I did hardly any error checking, I'll leave that up to you if you decide to use this method.

 

#include <WinAPI.au3>

Global Const $WATCH_PATH = "D:\Desktop\Test"

Global $t_Overlapped, $g_hDirectory, $g_hCompletion, $g_pCompletion, $g_pDataBuffer

Global Const $BUFFER_LENGTH = 4096
Global Const $NOTIFY_FLAGS  = BitOR($FILE_NOTIFY_CHANGE_FILE_NAME, $FILE_NOTIFY_CHANGE_DIR_NAME)

_EntryPoint()

Func _EntryPoint()
    $t_Overlapped = DllStructCreate($tagOVERLAPPED)
    $g_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))

    $g_hCompletion = DllCallbackRegister(_CompletionRoutine, "NONE", "DWORD;DWORD;PTR")
    $g_pCompletion = DllCallbackGetPtr($g_hCompletion)
    $g_pDataBuffer = _WinAPI_CreateBuffer($BUFFER_LENGTH)

    _ReadDirectoryChanges($g_hDirectory, $NOTIFY_FLAGS, $g_pDataBuffer, $BUFFER_LENGTH, 0, $t_Overlapped, $g_pCompletion)

    OnAutoItExitRegister(_FreeResources)

    While 1
        _SleepEx(10, 1)  ; Required!  Thread must enter an alertable state.  You could call this in an adlib if you dont require a loop.

        ; do loop stuff here

    WEnd
EndFunc

;~ 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

Func _FreeResources()
    _CancelIoEx($g_hDirectory, $t_Overlapped)
    _WinAPI_CloseHandle($g_hDirectory)
    _WinAPI_FreeMemory($g_pDataBuffer)
    DllCallbackFree($g_hCompletion)
    $t_Overlapped = 0
EndFunc

#region internal
Func _CompletionRoutine($nError, $nTransfered, $pOverlapped)
    Local $pBuffer = $g_pDataBuffer
    Local $tNotify
    Local $nLength

    While 1
        $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 $tNotify.NextOffset = 0 Then
            ExitLoop
        Else
            $pBuffer += $tNotify.NextOffset
        EndIf
    WEnd

    _ReadDirectoryChanges($g_hDirectory, $NOTIFY_FLAGS, $g_pDataBuffer, $BUFFER_LENGTH, 0, $t_Overlapped, $g_pCompletion)
EndFunc

Func _ReadDirectoryChanges($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

Func _SleepEx($nMilliseconds, $bAlertable = 0)
    Local $aRet = DllCall('kernel32.dll', 'dword', 'SleepEx', 'dword', $nMilliseconds, 'bool', $bAlertable)
    If @error Then Return SetError(@error, @extended, 0)
    Return $aRet[0]
EndFunc

Func _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
#endregion internal

 

Edited by Tekk
ms link was incorrect
Link to comment
Share on other sites

@Tekk - A thousand thanks for this. I won't be able to try to implement it until the weekend, but I'll put it into my script and will report the results.

@Nine - One last point about your excellent _CreateChild method. It took me a while to realize that this needs one change if it is to be used in a compiled script. Instead of using the macro @AutoItExe in the Run line near the end of the function, I need to FileCopy AutoIt3.exe into @TempDir and then use that copy of AutoIt3.exe to run the temporary script. If there's a simpler way to do this, I'll be glad to hear about it.

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

×
×
  • Create New...