프로그래밍 일반 2016. 8. 22. 14:52

FileSystemWatcher Changed 이벤트 중복 호출 버그

FileSystemWatcher 클래스는 특정 폴더나 파일 변경 모니터링이 필요할 때 사용하는데 Changed 이벤트에 버그가 있다. 


버그 해결이 까다로운데 이유는 해당 이벤트는 파일 변경이 종료되었음을 알리는게 아니기 때문에 종료 시점을 프로그래머가 꼼수를 써서 알아내야한다.


서버 환경 설정이 셋팅된 파일을 모니터링하다가 변경되면 실시간으로 반영하도록 작업되어 있는데 모니터링 파일이 커지면 Changed 이벤트가 


여러번 발생한다.


4,540 바이트 파일을 모니터링할 때 파일 복사가 종료될 때까지 LastWrite 필터는 5회, LastAccess 필터는 3회의 이벤트가 발생한다.


이벤트 중복 호출이 문제가 되는 이유는 일반적으로 변경 이벤트 발생 시 관련 핸들러에 해당 파일을 읽어 들이는 작업을 작성할텐데 파일 쓰기 


작업이 완료되지 않은 상황에 이벤트가 발생하여 파일에 동시 접근을 시도하기 때문에 변경 내용을 읽어들일 수 없게된다.


아래의 코드는 Changed 이벤트 버그를 해결할 수 있는 C#으로 작성한 간단한 예이다.  붉은색으로 작성한 부분이 중요 포인트


    /// <summary>

    /// 지정된 경로의 파일이나 폴더가 변경되면 이벤트를 발생 시킨다.

    /// </summary>

    public static class FileWatcher

    {

        /// <summary>

        /// 파일 시스템 모니터링 객체

        /// </summary>

        static FileSystemWatcher watcher = new FileSystemWatcher();



        /// <summary>

        /// 리소스 해제

        /// </summary>

        public static void Dispose()

        {

            watcher.Dispose();

        }


        /// <summary>

        /// 파일 시스템 이벤트 핸들러를 생성한다.

        /// </summary>

        /// <param name="dataDirectory">공통 csv 관리 폴더 경로</param>

        public static void CreateHandler(string dataDirectory)

        {

            if (false == watcher.EnableRaisingEvents)

            {

                watcher.Path = string.Format("{0}\\SystemConfig", dataDirectory);

                watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.Size;


    // 하나의 파일만 모니터링하고 있다.

                watcher.Filter = "ContentsConfig.ini";


                watcher.Changed += new FileSystemEventHandler(OnChanged);

                

                watcher.EnableRaisingEvents = true;

                watcher.IncludeSubdirectories = false;

            }

        }


        /// <summary>

        /// 지정된 경로에서 파일이나 디렉토리가 변경될 경우 발생하는 이벤트

        /// </summary>

        /// <param name="source"></param>

        /// <param name="eventArgs"></param>

        private static void OnChanged(object source, FileSystemEventArgs eventArgs)

        {

            try

            {

                // 모니터링을 비활성화, 해당 방법은 하나의 파일만 모니터링할 때 사용 가능하다.

                watcher.EnableRaisingEvents = false;


                if (WatcherChangeTypes.Changed != eventArgs.ChangeType)

                {

                    return;

                }


                // 파일 변경 모니터링 핸들러 진입


                // 서버 컨피그 파일만 처리한다.

                if (true == string.Equals(

                    "모니터링 파일 명",

                    eventArgs.Name,

                    StringComparison.OrdinalIgnoreCase))

                {

                    // 모니터링 파일이 큰 경우 FileSystemWatcher의 변경 이벤트가 중복 호출되는 버그와

                    // 변경 대응 이벤트가 무거운 경우 파일 액세스 동시 접근 오류가 발생한다.

                    // 동시 접근 오류를 해결하기 위해 쓰기가 완료되었는지 확인할 필요가 있다.

                    string fullpath = string.Format("{0}\\{1}", watcher.Path, SystemConfigDefine.ContentsConfigFileName);


                    // 파일 쓰기 중인지 확인한다.

                    while (true)

                    {

                        try

                        {

                            using (Stream stream = File.Open(fullpath, FileMode.Open, FileAccess.Read, FileShare.None))

                            {

                                if (null != stream)

                                {

                                    break;

                                }

                            }

                        }

                        catch (Exception exception)

                        {

                            LoggerWrapper.Information("[FileWatcher] Changing {0}, {1}", eventArgs.Name, exception.Message);

                        }


                        // 메인 스레드와 다른 스레드이기 때문에 슬립을 사용해도 다른 시스템에는 영향이 없다.

                        System.Threading.Thread.Sleep(1);

                    }


                    ... 이곳에 파일을 읽어들이는 코드를 작성한다.


         // 파일 변경 완료

                }


            }

            finally

            {

     // 모니터링 활성화

                watcher.EnableRaisingEvents = true;

            }


        }