[History]
 수정 - 기존 코드에 Event 가 누락되는 경우가 발생하여 코드 로직을 변경하였습니다.



안녕하세요?

이번 글에서는 Folder 를 감시하고 있다가, 파일이나 폴더에 변화가 온 것을 감지하는 방법에 대해서 알아보겠습니다.

간단히 방법을 설명드리자면, 핵심 Windows API 는 ReadDirectoryChanges 를 이용하는 것입니다.

예전에 비슷한 내용으로 http://crystalcube.co.kr/entry/JNI-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-Folder-Monitoring 를 통해서 방법을 설명 드렸습니다.
그런데 이 방법은 Folder 변화만 감지할 뿐, 세부적으로 어떤 파일이 어떻게 변경되었는지는 알 수 없습니다.

이번시간에 소개시켜드리는 방법은, 변경된 파일의 정보까지도 얻어올 수 있습니다.

기본적인 jni 빌드 방법은 위에서 언급한 이전글을 참고하시기 바랍니다.


1.  FolderWatcher.java - java 에서 직접적으로 호출하게 될 최종 클래스입니다.


package crystalcube.win32;
 
import java.util.ArrayList;
 
 
public class FolderWatcher implements Runnable {
    // Action(For ReadDirectoryChanges)
    public final static int FILE_ACTION_ADDED = 0x00000001;
    public final static int FILE_ACTION_REMOVED = 0x00000002;
    public final static int FILE_ACTION_MODIFIED = 0x00000003;
    public final static int FILE_ACTION_RENAMED_OLD_NAME = 0x00000004;
    public final static int FILE_ACTION_RENAMED_NEW_NAME = 0x00000005;
     
    // Filter(For ReadDirctoryChanges)
    public final static int FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001;
    public final static int FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002;
    public final static int FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004;
    public final static int FILE_NOTIFY_CHANGE_SIZE = 0x00000008;
    public final static int FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010;
    public final static int FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x00000020;
    public final static int FILE_NOTIFY_CHANGE_CREATION = 0x00000040;
    public final static int FILE_NOTIFY_CHANGE_SECURITY = 0x00000100;
     
    // enum
    public static enum Action { FILE_ACTION_ADDED, FILE_ACTION_REMOVED, FILE_ACTION_MODIFIED, FILE_ACTION_RENAMED_OLD_NAME, FILE_ACTION_RENAMED_NEW_NAME };
     
    // win32 api
    private native ArrayList<Pair> ReadDirectoryChangesW(int handle, boolean watchSubtree, int filter);
    private native int CreateFile(String folder);
    private native boolean CloseHandle(int handle);
     
    // Filter
    private final static int ReadDirectoryChangesFilter =
        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;
     
    // jni
    static { System.loadLibrary("FolderWatcher"); }
     
    // listener
    private IFolderChangeListener listener = null;
     
    private boolean isWatching = false;
    //private int handle = 0;
    private volatile Object lockObj = new Object();
     
    private String folderPath = "";
    private boolean watchSubfolder = false;
    private int filter;
    
    private int hDirectory = -1;
     
    private Thread watchThread = null;
     
     
    //============
    // 생성자
    //============
    public FolderWatcher(IFolderChangeListener listener) {
        this.listener = listener;
    }
     
     
    private static String appendSlash(String path) {
        if(path.endsWith("\\") == false) {
            return path + "\\";
        }
        return path;
    }
     
     
     
    public boolean start(String folderPath, boolean watchSubfolder, int filter) {
        synchronized (this.lockObj) {
            // 이미 실행중인 경우
            if(isWatching == true) { return true; }
             
            // setting
            this.folderPath = FolderWatcher.appendSlash(folderPath);
            this.watchSubfolder = watchSubfolder;
            this.filter = filter;
            
            // Handle 설정
            this.hDirectory = CreateFile(this.folderPath);
            if(this.hDirectory ==  -1) { return false; }
             
            // Thread 실행
            this.watchThread = new Thread(this);
            this.watchThread.setDaemon(true);
            this.watchThread.setName("FolderWatcher");
            this.watchThread.start();
             
            return true;
        }
    }
     
     
    @Override
    public void run(){
        this.isWatching = true;
         
        while(Thread.currentThread().isInterrupted() == false && this.isWatching == true) {
            ArrayList<Pair> result = ReadDirectoryChangesW(this.hDirectory, this.watchSubfolder, this.filter);
 
            // Stop 체크
            if(Thread.currentThread().isInterrupted() == true || this.isWatching == false) {
                break;
            }

            for(Pair obj : result) {
                OnReadChanges(obj.getFirst(), obj.getSecond());
            }
        }
        
        // Handle Close
        if(this.hDirectory != -1) {
        	if(CloseHandle(this.hDirectory) == true) {
        		this.hDirectory = -1;
        	}
        }
    }
     
     
    public boolean stop() {
        synchronized (this.lockObj) {
            if(this.isWatching == false) { return true; }
            //stopWatch();
            this.isWatching = false;
            this.watchThread.interrupt();
             
            return true;
        }
    }
     
     
    private void OnReadChanges(String fileName, int action) {
        if(this.listener != null) {
            Action a = Action.FILE_ACTION_ADDED;
            switch(action) {
                case FILE_ACTION_ADDED: a = Action.FILE_ACTION_ADDED; break;
                case FILE_ACTION_REMOVED: a = Action.FILE_ACTION_REMOVED; break;
                case FILE_ACTION_MODIFIED: a = Action.FILE_ACTION_MODIFIED; break;
                case FILE_ACTION_RENAMED_OLD_NAME: a = Action.FILE_ACTION_RENAMED_OLD_NAME; break;
                case FILE_ACTION_RENAMED_NEW_NAME: a = Action.FILE_ACTION_RENAMED_NEW_NAME; break;
            }
            
            // folder 와 filename 분리
            String path = this.folderPath;
            String file = fileName;
            String fullPath = path + file;
            if(fileName.contains("\\") == true) {
            	int pos = fullPath.lastIndexOf("\\");
            	path = fullPath.substring(0, pos+1);
            	file = fullPath.substring(pos+1, fullPath.length());
            }
             
            this.listener.folderChanged(this, path, file, fullPath, a);
        }
    }
     
     
    @Override
    protected void finalize() throws Throwable {
        try {
            this.stop();
        } catch(Exception e) {}
        super.finalize();
    }
}






2. IFolderChangeListener - 폴더 변경시 호출되는 콜백을 정의한 인터페이스


package crystalcube.win32;

import crystalcube.win32.FolderWatcher.Action;

public interface IFolderChangeListener {
	public void folderChanged(Object sender, String fileName, Action action);
}



 

3. crystalcube_win32_FolderWatcher.h - javah 를 통해서 생성된 C 의 header 파일

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class crystalcube_win32_FolderWatcher */

#ifndef _Included_crystalcube_win32_FolderWatcher
#define _Included_crystalcube_win32_FolderWatcher
#ifdef __cplusplus
extern "C" {
#endif
#undef crystalcube_win32_FolderWatcher_FILE_ACTION_ADDED
#define crystalcube_win32_FolderWatcher_FILE_ACTION_ADDED 1L
#undef crystalcube_win32_FolderWatcher_FILE_ACTION_REMOVED
#define crystalcube_win32_FolderWatcher_FILE_ACTION_REMOVED 2L
#undef crystalcube_win32_FolderWatcher_FILE_ACTION_MODIFIED
#define crystalcube_win32_FolderWatcher_FILE_ACTION_MODIFIED 3L
#undef crystalcube_win32_FolderWatcher_FILE_ACTION_RENAMED_OLD_NAME
#define crystalcube_win32_FolderWatcher_FILE_ACTION_RENAMED_OLD_NAME 4L
#undef crystalcube_win32_FolderWatcher_FILE_ACTION_RENAMED_NEW_NAME
#define crystalcube_win32_FolderWatcher_FILE_ACTION_RENAMED_NEW_NAME 5L
#undef crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_FILE_NAME
#define crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_FILE_NAME 1L
#undef crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_DIR_NAME
#define crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_DIR_NAME 2L
#undef crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_ATTRIBUTES
#define crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_ATTRIBUTES 4L
#undef crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_SIZE
#define crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_SIZE 8L
#undef crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_LAST_WRITE
#define crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_LAST_WRITE 16L
#undef crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_LAST_ACCESS
#define crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_LAST_ACCESS 32L
#undef crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_CREATION
#define crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_CREATION 64L
#undef crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_SECURITY
#define crystalcube_win32_FolderWatcher_FILE_NOTIFY_CHANGE_SECURITY 256L
#undef crystalcube_win32_FolderWatcher_ReadDirectoryChangesFilter
#define crystalcube_win32_FolderWatcher_ReadDirectoryChangesFilter 383L
/*
 * Class:     crystalcube_win32_FolderWatcher
 * Method:    ReadDirectoryChangesW
 * Signature: (IZI)Ljava/util/ArrayList;
 */
JNIEXPORT jobject JNICALL Java_crystalcube_win32_FolderWatcher_ReadDirectoryChangesW
  (JNIEnv *, jobject, jint, jboolean, jint);

/*
 * Class:     crystalcube_win32_FolderWatcher
 * Method:    CreateFile
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_crystalcube_win32_FolderWatcher_CreateFile
  (JNIEnv *, jobject, jstring);

/*
 * Class:     crystalcube_win32_FolderWatcher
 * Method:    CloseHandle
 * Signature: (I)Z
 */
JNIEXPORT jboolean JNICALL Java_crystalcube_win32_FolderWatcher_CloseHandle
  (JNIEnv *, jobject, jint);

#ifdef __cplusplus
}
#endif
#endif




 

4. FolderWatcher.cpp - header 를 구현한 C 파일


#include "crystalcube_win32_FolderWatcher.h"
#include <windows.h>
#include <iostream>

using namespace std;


/*
 * Class:     crystalcube_win32_FolderWatcher
 * Method:    CreateFile
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_crystalcube_win32_FolderWatcher_CreateFile
  (JNIEnv * env, jobject obj, jstring path)
{
	const jchar* new_path = env->GetStringChars(path, NULL);

	// Directory Handle 생성
	HANDLE hDirectory = CreateFile((LPCWSTR)new_path,
		FILE_LIST_DIRECTORY,
		FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
		NULL,
		OPEN_EXISTING,
		FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
		NULL);

	return (int)hDirectory;
}



/*
 * Class:     crystalcube_win32_FolderWatcher
 * Method:    CloseHandle
 * Signature: (I)Z
 */
JNIEXPORT jboolean JNICALL Java_crystalcube_win32_FolderWatcher_CloseHandle
  (JNIEnv * env, jobject obj, jint hDirectory)
{
	return CloseHandle((HANDLE)hDirectory);
}





/*
 * Class:     crystalcube_win32_FolderWatcher
 * Method:    ReadDirectoryChangesW
 * Signature: (IZI)Ljava/util/ArrayList;
 */
JNIEXPORT jobject JNICALL Java_crystalcube_win32_FolderWatcher_ReadDirectoryChangesW
  (JNIEnv * env, jobject obj, jint handle, jboolean watchSubtree, jint filter)
{
	HANDLE hDirectory = (HANDLE)handle;

	// Create java.util.ArrayList
	jclass listClass = env->FindClass("java/util/ArrayList");
	jmethodID init = env->GetMethodID(listClass, "<init>", "()V");
	jobject pairList = env->NewObject(listClass, init);

	//set put Method
	jmethodID add = env->GetMethodID(listClass, "add", "(Ljava/lang/Object;)Z");


	// Create pcloud.win32.Pair
	jclass pairClass = env->FindClass("pcloud/win32/Pair");
	jmethodID initPair = env->GetMethodID(pairClass, "<init>", "(Ljava/lang/String;I)V");
	//jobject jPair;


	// Create java.lang.Integer
	jclass integerClass = env->FindClass("java/lang/Integer");
	jmethodID initInteger = env->GetMethodID(integerClass, "<init>", "(I)V");

	CONST DWORD cbBuffer = 32*1024;
	BYTE pBuffer[cbBuffer];
	DWORD bytesReturned;

	//LPVOID lpBuffer;
	FILE_NOTIFY_INFORMATION* pfni;

	// Directory Handle 체크
	if(hDirectory == INVALID_HANDLE_VALUE) { OutputDebugStr(L"[FolderWatcher] INVALID HANDLE."); return pairList; }

	// 버퍼 할당 & check
	//pBuffer = (PBYTE)malloc(cbBuffer);
	if(pBuffer == NULL) { OutputDebugStr(L"[FolderWatcher] Memory alloc fail."); return pairList; }

	// 함수 호출
	BOOL readResult = ReadDirectoryChangesW(hDirectory, pBuffer, cbBuffer, (BOOL)watchSubtree, filter, &bytesReturned, 0, 0);
	if(readResult == FALSE) { 
		OutputDebugStr(L"[FolderWatcher] ERROR_INVALID_FUNCTION - ReadDirectoryChangesW."); 
		// release memory
		//free(pBuffer);
		return pairList;
	}


	// 변경된 내역 가져옴
	pfni = (FILE_NOTIFY_INFORMATION*)pBuffer;
	do {
		//if(pfni->Action = 0x00000001) {
		//	WaitForFile(pfni->FileName);
		//}

		// java 용 String 생성
		//OutputDebugStr(L"[FolderWatcher] java 용 String 생성.");
		jobject jFilePath = env->NewString((jchar*)pfni->FileName, pfni->FileNameLength/2);

		// java용 Pair 생성
		//OutputDebugStr(L"[FolderWatcher] java용 Pair 생성.");
		jobject jPair = env->NewObject(pairClass, initPair, jFilePath, pfni->Action);

		// list 에 추가
		//OutputDebugStr(L"[FolderWatcher] list 에 추가.");
		env->CallObjectMethod(pairList, add, jPair);

		//pfni = (FILE_NOTIFY_INFORMATION*)((PBYTE)pfni + pfni->NextEntryOffset);

		if(pfni->NextEntryOffset <= 0) {
			//OutputDebugStr(L"[FolderWatcher] pfni->NextEntryOffset <= 0.");
			pfni = NULL;
		} else {
			//OutputDebugStr(L"[FolderWatcher] pfni->NextEntryOffset.");
			pfni = (FILE_NOTIFY_INFORMATION*)((PBYTE)pfni + pfni->NextEntryOffset);
		}
	} while(pfni != NULL);

	// release memory
	//OutputDebugStr(L"[FolderWatcher] release memory.");
	//free(pBuffer);

	return pairList;
}






코드가 간단하므로 자세한 설명은 생략하도록 하겠습니다.
그럼 사용방법을 알아보죠~


실제 사용 Example


// Written by sw0826.kim@samsung.com
package crystalcube.win32;

import crystalcube.win32.FolderWatcher.Action;


public class WatcherTestMain implements IFolderChangeListener{
	public static void main(String[] args)
	{
		WatcherTestMain test = new WatcherTestMain();
		test.test();
	}
	
	public void test() {
		int ReadDirectoryChangesFilter =
			FolderWatcher.FILE_NOTIFY_CHANGE_FILE_NAME |
			FolderWatcher.FILE_NOTIFY_CHANGE_DIR_NAME |
			FolderWatcher.FILE_NOTIFY_CHANGE_ATTRIBUTES |
			FolderWatcher.FILE_NOTIFY_CHANGE_SIZE |
			FolderWatcher.FILE_NOTIFY_CHANGE_LAST_WRITE |
			FolderWatcher.FILE_NOTIFY_CHANGE_LAST_ACCESS |
			FolderWatcher.FILE_NOTIFY_CHANGE_CREATION |
			FolderWatcher.FILE_NOTIFY_CHANGE_SECURITY;
		
		FolderWatcher watcher = new FolderWatcher(this);
		watcher.start("D:\\woogi", true, ReadDirectoryChangesFilter);

//		
//		FolderWatcher watcher2 = new FolderWatcher(this);
//		watcher2.start("C:\\", false, FolderWatcher.FILTER_CHANGE_DIR_NAME | FolderWatcher.FILTER_CHANGE_FILE_NAME);
		try {
			Thread.sleep(3000000);
//			watcher.stop();
//			Thread.sleep(300000);
//			//watcher2.stop();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void folderChanged(Object sender, String fileName, Action action) {
		System.out.println("changed: " + fileName + " -- " + action.toString());
	}
}





이상으로 블로깅을 마칩니다. :)