Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

[AHK_L/v2] WatchDirectory - Report Directory Changes


  • Please log in to reply
158 replies to this topic
sebastian___
  • Members
  • 23 posts
  • Last active: Oct 06 2015 09:38 PM
  • Joined: 05 Oct 2007
Sorry but the ANSI version still doesn't work - it truncates the file name reported. But that's ok, because the "Unicode x86" version works.

But your "above post" links to my post :) and as I said I tried your script but I still get two message box with the same message.

If I delete a file from the watched folder - I get just one messagebox.
But If I save a jpg with Paint in that folder I get 5 message boxes ! With the same message : "J:\img Convert\rename\Winter.jpg"

So by saving from Paint I get 5 messages, but if I move the file in the folder with drag and drop - I get only 2 msxbox.

Maybe I need to set some delay option ?

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Which ANSI AHK version do you use? I have tried both AHK_L+H 1.0.92.02 and my example seems to work fine.

I tried saving a jpg in paint, you can see what is happening using my example in first post, it looks like a file is created, deleted, created again and changed than so this results in 4 reports.
So what would you like to see when it gets reported? Only the first report or only last report?

sebastian___
  • Members
  • 23 posts
  • Last active: Oct 06 2015 09:38 PM
  • Joined: 05 Oct 2007
I used the ANSI x86 from this link "AutoHotkey_La.zip":
<!-- m -->https://ahknet.autoh...otkey_L/#Get_It<!-- m -->

But it's fine, because as I said the "Unicode x86" from the same page - works.

I need just one report.
But I just tried saving a file with Notepad and then with XNView in different formats and it seems the number of reports can change depending of the application. So maybe a way to control how many reports you get for any application would be good.

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Try that, just replace it in example script.
Alternatively you can replace red code with yours.
ReportChanges(from, to){
   static _from:=Object(),_to:=Object(), i
	[color=red]global TotalChanges[/color]
   i:=_to.MaxIndex()
   if (_from[i]="" && _to[i]=from && to="") ;created -> deleted (do not report)
		Return,_from.Remove(i),_to.Remove(i)
	else if (_from[i]="" && from=to && _to[i]=to){ ;created -> changed (report created only)
		SetTimer,Report,-1000
	} else if (_from[i]=from && to="" && _to[i]=from){ ;changed -> deleted (report deletion only)
		_from[i]:=from,_to[i]:=to
		SetTimer,Report,-1000
	} else If !(_from[i]=from && _to[i]=to){ ;no report for multiple modifications
      _from.Insert(from),_to.Insert(to)
      i++
      SetTimer,Report,-1000
	} 
   Return
   Report:
      If (i:=_to.MaxIndex()) {
			[color=red]Gui,ListView, ChangesList
			LV_Insert(1,"",A_Hour ":" A_Min ":" A_Sec ":" A_MSec,_from[i],_to[i])
			LV_ModifyCol()
			LV_ModifyCol(1,"AutoHdr"),LV_ModifyCol(2,"AutoHdr")
			TotalChanges++
			SB_SetText("Changes Registered " . TotalChanges)[/color]
			_from.Remove(i),_to.Remove(i)
			If i:=_to.MaxIndex()
				SetTimer,Report,-30
		}
   Return
}


sebastian___
  • Members
  • 23 posts
  • Last active: Oct 06 2015 09:38 PM
  • Joined: 05 Oct 2007
Thank you. Now it works. But it has a few limitations. For example I can't send to the watch folder - two or more files at once.
And I can't send files too fast. If I start to send let's say one file every 100 miliseconds, I won't get all reports. More than half of the files won't be reported. I can't tweak that a little with the timing values from your script ( SetTimer,Report,-1000) but even if I set the value -2 I still won't get all the reports.

Still, it's usable for me, so thank you.

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
Is it possible to watch for a change in a single file?
This is what I tried unsuccessfully:
WatchDirectory("C:\Users\Chris\AppData\Local\Temp\7plus|test.jpg\", "ImageConverter_OpenedFileChange")

The function doesn't get called.

Edit: Single file works in an extra test script, but I still don't get the notifications in my real program...

Edit: Some debugging revealed to me that FileExist failes in this line:
if !InStr(FileExist(dir1),"D")
My application is running as admin, could this be an issue?

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
If you like you can pm your program to me and I can take a look ;)

Edit:
Does dir1 contain correct path? What does FileExist return for you?

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
I'm afraid my program is a bit too large for this. If nothing helps, I can commit the latest revision or build a release version for you.

The path in dir1 is correct, but FileExist returns an empty value.
I tried running my program as normal user with the same result.

And another edit: Now I don't see the function returning there anymore, but I had a strange permission issue with this folder which was fixed after a reboot. The problem with this function seems to lie elsewhere. I'll check where it hangs now.

And another one: The function returns here the first and each subsequent timed call
if !( (r:=DllCall("MsgWaitForMultipleObjectsEx","UInt",#.MaxIndex() 
               ,"UInt",DirEvents[],"UInt",0,"UInt",0x4FF,"UInt",6))>=0 
               && r<#.MaxIndex() ){ 
         return
      }

Is this correct? I suspect this means that it didn't see changes?

One more edit: I now tried a simple path in my main program on the same line as before and it did not work either.

- Tried putting a simple WatchDirectory close to the beginning of my program and the notification function after the autoexecute section. With this setup I get the notifications before/after/during my OnExit routine. Maybe they queue up somehow? I have a constantly running loop in my program (with sleep on each iteration).

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Can you try using SetTimer instead of your loop?

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
I'm not sure how good this will work, I think I had some issues with that earlier. I managed to reproduce this behavior in an example script though and I can confirm that the loop is the cause.
#include %A_ScriptDir%\lib\Struct.ahk
while(!Exit)
{
	if(A_Index = 1)
		WatchDirectory("C:\test|clip.jpg\", "ImageConverter_OpenedFileChange")
	Sleep 1000
}
return
#g::Exit := true

ImageConverter_OpenedFileChange(from, to)
{
	msgbox change %from% %to%
}


;By HotKeyIt
;Docs: http://www.autohotkey.com/forum/viewtopic.php?p=398565#398565
WatchDirectory(p*){ 
   ;Structures 
   static FILE_NOTIFY_INFORMATION:="DWORD NextEntryOffset,DWORD Action,DWORD FileNameLength,WCHAR FileName[1]" 
   static OVERLAPPED:="ULONG_PTR Internal,ULONG_PTR InternalHigh,{{DWORD offset,DWORD offsetHigh},PVOID Pointer},HANDLE hEvent" 
   ;Variables 
   static running,sizeof_FNI=65536,nReadLen:=VarSetCapacity(nReadLen,8),WatchDirectory:=RegisterCallback("WatchDirectory","F",0,0) 
   static timer,ReportToFunction,LP,nReadLen:=VarSetCapacity(LP,(260)*(A_PtrSize/2),0) 
   static @:=Object(),reconnect:=Object(),#:=Object(),DirEvents,StringToRegEx="\\\|.\.|+\+|[\[|{\{|(\(|)\)|^\^|$\$|?\.?|*.*" 
   ;ReadDirectoryChanges related 
   static FILE_NOTIFY_CHANGE_FILE_NAME=0x1,FILE_NOTIFY_CHANGE_DIR_NAME=0x2,FILE_NOTIFY_CHANGE_ATTRIBUTES=0x4 
         ,FILE_NOTIFY_CHANGE_SIZE=0x8,FILE_NOTIFY_CHANGE_LAST_WRITE=0x10,FILE_NOTIFY_CHANGE_CREATION=0x40 
         ,FILE_NOTIFY_CHANGE_SECURITY=0x100 
   static FILE_ACTION_ADDED=1,FILE_ACTION_REMOVED=2,FILE_ACTION_MODIFIED=3 
         ,FILE_ACTION_RENAMED_OLD_NAME=4,FILE_ACTION_RENAMED_NEW_NAME=5 
   static OPEN_EXISTING=3,FILE_FLAG_BACKUP_SEMANTICS=0x2000000,FILE_FLAG_OVERLAPPED=0x40000000 
         ,FILE_SHARE_DELETE=4,FILE_SHARE_WRITE=2,FILE_SHARE_READ=1,FILE_LIST_DIRECTORY=1 
   If p.MaxIndex(){ 
      If (p.MaxIndex()=1 && p.1=""){ 
         for i,folder in # 
            DllCall("CloseHandle","Uint",@[folder].hD),DllCall("CloseHandle","Uint",@[folder].O.hEvent) 
            ,@.Remove(folder) 
         #:=Object() 
         DirEvents:=Struct("HANDLE[1000]") 
         DllCall("KillTimer","Uint",0,"Uint",timer) 
         timer= 
         Return 0 
      } else { 
         if p.2 
            ReportToFunction:=p.2 
         If !IsFunc(ReportToFunction) 
            Return -1 ;DllCall("MessageBox","Uint",0,"Str","Function " ReportToFunction " does not exist","Str","Error Missing Function","UInt",0) 
         RegExMatch(p.1,"^([^/\*\?<>\|""]+)(\*)?(\|.+)?$",dir) 
         if (SubStr(dir1,0)="\") 
            StringTrimRight,dir1,dir1,1 
         StringTrimLeft,dir3,dir3,1 
         If (p.MaxIndex()=2 && p.2=""){ 
            for i,folder in # 
               If (dir1=SubStr(folder,1,StrLen(folder)-1)) 
                  Return 0 ,DirEvents[i]:=DirEvents[#.MaxIndex()],DirEvents[#.MaxIndex()]:=0 
                           @.Remove(folder),#[i]:=#[#.MaxIndex()],#.Remove(i) 
            Return 0 
         } 
      } 
      if !InStr(FileExist(dir1),"D") 
         Return -1 ;DllCall("MessageBox","Uint",0,"Str","Folder " dir1 " does not exist","Str","Error Missing File","UInt",0) 
      for i,folder in # 
      { 
         If (dir1=SubStr(folder,1,StrLen(folder)-1) || (InStr(dir1,folder) && @[folder].sD)) 
               Return 0 
         else if (InStr(SubStr(folder,1,StrLen(folder)-1),dir1 "\") && dir2){ ;replace watch 
            DllCall("CloseHandle","Uint",@[folder].hD),DllCall("CloseHandle","Uint",@[folder].O.hEvent),reset:=i 
         } 
      } 
      LP:=SubStr(LP,1,DllCall("GetLongPathName","Str",dir1,"Uint",&LP,"Uint",VarSetCapacity(LP))) "\" 
      If !(reset && @[reset]:=LP) 
         #.Insert(LP) 
      @[LP,"dir"]:=LP 
      @[LP].hD:=DllCall("CreateFile","Str",StrLen(LP)=3?SubStr(LP,1,2):LP,"UInt",0x1,"UInt",0x1|0x2|0x4 
                  ,"UInt",0,"UInt",0x3,"UInt",0x2000000|0x40000000,"UInt",0) 
      @[LP].sD:=(dir2=""?0:1) 

      Loop,Parse,StringToRegEx,| 
         StringReplace,dir3,dir3,% SubStr(A_LoopField,1,1),% SubStr(A_LoopField,2),A 
      StringReplace,dir3,dir3,%A_Space%,\s,A 
      Loop,Parse,dir3,| 
      { 
         If A_Index=1 
            dir3= 
         pre:=(SubStr(A_LoopField,1,2)="\\"?2:0) 
         succ:=(SubStr(A_LoopField,-1)="\\"?2:0) 
         dir3.=(dir3?"|":"") (pre?"\\\K":"") 
               . SubStr(A_LoopField,1+pre,StrLen(A_LoopField)-pre-succ) 
               . ((!succ && !InStr(SubStr(A_LoopField,1+pre,StrLen(A_LoopField)-pre-succ),"\"))?"[^\\]*$":"") (succ?"$":"") 
      } 
      @[LP].FLT:="i)" dir3 
      @[LP].FUNC:=ReportToFunction 
      @[LP].CNG:=(p.3?p.3:(0x1|0x2|0x4|0x8|0x10|0x40|0x100)) 
      If !reset { 
         @[LP].SetCapacity("pFNI",sizeof_FNI) 
         @[LP].FNI:=Struct(FILE_NOTIFY_INFORMATION,@[LP].GetAddress("pFNI")) 
         @[LP].O:=Struct(OVERLAPPED) 
      } 
      @[LP].O.hEvent:=DllCall("CreateEvent","Uint",0,"Int",1,"Int",0,"UInt",0) 
      If (!DirEvents) 
         DirEvents:=Struct("HANDLE[1000]") 
      DirEvents[reset?reset:#.MaxIndex()]:[email protected][LP].O.hEvent 
      DllCall("ReadDirectoryChangesW","UInt",@[LP].hD,"UInt",@[LP].FNI[],"UInt",sizeof_FNI 
               ,"Int",@[LP].sD,"UInt",@[LP].CNG,"UInt",0,"UInt",@[LP].O[],"UInt",0) 
      Return timer:=DllCall("SetTimer","Uint",0,"UInt",timer,"Uint",50,"UInt",WatchDirectory) 
   } else { 
      for LP in reconnect 
      { 
         If (FileExist(@[LP].dir) && reconnect.Remove(LP)){ 
            DllCall("CloseHandle","Uint",@[LP].hD) 
            @[LP].hD:=DllCall("CreateFile","Str",StrLen(@[LP].dir)=3?SubStr(@[LP].dir,1,2):@[LP].dir,"UInt",0x1,"UInt",0x1|0x2|0x4 
                  ,"UInt",0,"UInt",0x3,"UInt",0x2000000|0x40000000,"UInt",0) 
            DllCall("ResetEvent","UInt",@[LP].O.hEvent) 
            DllCall("ReadDirectoryChangesW","UInt",@[LP].hD,"UInt",@[LP].FNI[],"UInt",sizeof_FNI 
               ,"Int",@[LP].sD,"UInt",@[LP].CNG,"UInt",0,"UInt",@[LP].O[],"UInt",0) 
         } 
      } 
      if !( (r:=DllCall("MsgWaitForMultipleObjectsEx","UInt",#.MaxIndex() 
               ,"UInt",DirEvents[],"UInt",0,"UInt",0x4FF,"UInt",6))>=0 
               && r<#.MaxIndex() ){ 
         return 
      } 
      DllCall("KillTimer", UInt,0, UInt,timer) 
      LP:=#[r+1],DllCall("GetOverlappedResult","UInt",@[LP].hD,"UInt",@[LP].O[],"UIntP",nReadLen,"Int",1) 
      If (A_LastError=64){ ; ERROR_NETNAME_DELETED - The specified network name is no longer available. 
         If !FileExist(@[LP].dir) ; If folder does not exist add to reconnect routine 
            reconnect.Insert(LP,LP) 
      } else 
         Loop { 
            FNI:=A_Index>1?Struct(FILE_NOTIFY_INFORMATION,FNI[]+FNI.NextEntryOffset):Struct(FILE_NOTIFY_INFORMATION,@[LP].FNI[]) 
            If (FNI.Action < 0x6){ 
               FileName:[email protected][LP].dir . StrGet(FNI.FileName[""],FNI.FileNameLength/2,"UTF-16") 
               If (FNI.Action=FILE_ACTION_RENAMED_OLD_NAME) 
                     FileFromOptional:=FileName 
               If (@[LP].FLT="" || RegExMatch(FileName,@[LP].FLT) || FileFrom) 
                  If (FNI.Action=FILE_ACTION_ADDED){ 
                     FileTo:=FileName 
                  } else If (FNI.Action=FILE_ACTION_REMOVED){ 
                     FileFrom:=FileName 
                  } else If (FNI.Action=FILE_ACTION_MODIFIED){ 
                     FileFrom:=FileTo:=FileName 
                  } else If (FNI.Action=FILE_ACTION_RENAMED_OLD_NAME){ 
                     FileFrom:=FileName 
                  } else If (FNI.Action=FILE_ACTION_RENAMED_NEW_NAME){ 
                     FileTo:=FileName 
                  } 
          If (FNI.Action != 4 && (FileTo . FileFrom) !="") 
                  @[LP].Func(FileFrom=""?FileFromOptional:FileFrom,FileTo) 
            } 
         } Until (!FNI.NextEntryOffset || ((FNI[]+FNI.NextEntryOffset) > (@[LP].FNI[]+sizeof_FNI-12))) 
      DllCall("ResetEvent","UInt",@[LP].O.hEvent) 
      DllCall("ReadDirectoryChangesW","UInt",@[LP].hD,"UInt",@[LP].FNI[],"UInt",sizeof_FNI 
               ,"Int",@[LP].sD,"UInt",@[LP].CNG,"UInt",0,"UInt",@[LP].O[],"UInt",0) 
      timer:=DllCall("SetTimer","Uint",0,"UInt",timer,"Uint",50,"UInt",WatchDirectory) 
      Return 
   } 
   Return 
}

It will not see the messages until #g is pressed and the loop returns.
Do you think that it might be possible to make it work even with a loop?
I'm not sure if I can get rid of it completely. It's a kind of scheduler that processes events.

A (hackish) alternative would be to create a watching AHK process that notifies my main program about it.

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
I tried replacing my while loop with a timer, it's working surprisingly well so far, but maybe I just forgot about the problems and didn't see them yet :D

However, it still behaves the same as with the loop. Notifications are only seen once I reload the script.

First few lines of demo script:
#include %A_ScriptDir%\lib\Struct.ahk
#persistent
SetTimer, loop, 100
WatchDirectory("C:\test|clip.jpg\", "ImageConverter_OpenedFileChange")
return

loop:
if(Exit)
	SetTimer, Loop, Off
return
#g::Exit := true

ImageConverter_OpenedFileChange(from, to)
{
	msgbox change %from% %to%
}

Is it possible that it must not have any timers at all?

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
A fix was very simple but without your help I do not think I would have ever found it. Thank you very much fragman :D

We simply need to process messages when function is called so I now Sleep,0 is done when function is called to process notifications.

fragman
  • Members
  • 1591 posts
  • Last active: Nov 12 2012 08:51 PM
  • Joined: 13 Oct 2009
Edit: All is fine now! Thank you very much :)

Btw, here's what I use it for:
<!-- m -->http://i.imgur.com/KnbTy.jpg<!-- m -->

It's a batch image converter/screenshot tool window with FTP/ImgUr upload functionality. Clicking a picture opens it in the associated image editor and 7plus will watch for changes and refresh the image on the gui when it changes :)

flashkid
  • Members
  • 115 posts
  • Last active: Apr 12 2013 06:33 PM
  • Joined: 25 Aug 2007
Very nice function, works very well.
But I have one serious problem. If I compile this script, it does no longer work (tested with your example script).
I don't receive any notifications about changed files.
If I run the same script, without compiling, at the same time I get the notifications. Can you confirm this? I tested it on 2 computers.

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Thanks flashkid, which AHK (AutoHotkeySC.bin) version are you using?