首页 > 解决方案 > C# File System Watcher windows 服务发生内存泄漏,无法追踪导致内存泄漏的原因

问题描述

在过去的几天里,我一直在监视我创建的 Windows 服务,因为我确信它存在内存泄漏。事实证明,我是对的——在过去的几天里,它的内存使用量从 41MB 增加到了 75MB。

该服务所做的只是查看文件目录,每次在该目录中创建文件时,都会将其上传到我们的内容管理系统(Microfocus 内容管理器);其他一些其他任务也正在执行,例如如果发生某些异常,则写入事件日志并将有关上传状态的消息写入日志文件。

我想尝试用来查找此泄漏的一个想法是使用 .NET CLR 分析器之类的东西,正如在回答这个问题时提出的那样。但是,分析器似乎不适用于 Windows 服务(所以我需要以某种方式将我制作的服务更改为控制台应用程序?)而且我不确定它是否能够在应用程序运行时对其进行分析?

无论如何,这里是 Windows 服务代码的完整副本。我感谢任何能够阅读此内容并查看是否有我所做的愚蠢行为导致泄漏的人,在此下面我将讨论我认为可能导致内存泄漏的区域。

using HP.HPTRIM.SDK;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Configuration;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace BailiffReturnsUploader
{
    public partial class BailiffReturnsService : ServiceBase
    {
        private string FileWatchPath = ConfigurationManager.AppSettings["FileWatchPath"];
        private string UploadLogPath = ConfigurationManager.AppSettings["UploadLogPath"];
        private string UploadErrorLocation = ConfigurationManager.AppSettings["UploadErrorLocation"];
        private string ContentManagerEnviro = ConfigurationManager.AppSettings["ContentManagerEnviro"];

        public BailiffReturnsService()
        {
            InitializeComponent();
            bailiffEventLogger = new EventLog();
            if(!EventLog.SourceExists("BailiffReturnsSource"))
            {
                EventLog.CreateEventSource("BailiffReturnsSource", "BailiffReturnsLog");
            }
            bailiffEventLogger.Source = "BailiffReturnsSource";
            bailiffEventLogger.Log = "BailiffReturnsLog";
        }

        protected override void OnStart(string[] args)
        {
            try
            {
                TrimApplication.Initialize();

                BailiffReturnsFileWatcher = new FileSystemWatcher(FileWatchPath)
                {
                    NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.Attributes,
                    Filter = "*.*"
                };
                // I think this is the problematic part, the event registration?
                BailiffReturnsFileWatcher.Created += new FileSystemEventHandler(OnCreate);
                BailiffReturnsFileWatcher.EnableRaisingEvents = true;

                bailiffEventLogger.WriteEntry("Service has started", EventLogEntryType.Information);
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(string.Format("Could not create file listener : {0}", ex.Message), EventLogEntryType.Error);
            }
        }

        protected override void OnStop()
        {
            bailiffEventLogger.WriteEntry("Service has stopped", EventLogEntryType.Information);
            Dispose();
        }

        /// <summary>
        /// Handler for file
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void OnCreate(object sender, FileSystemEventArgs e)
        {
            try
            {
                int attempts = 0;
                FileInfo fileInfo = new FileInfo(e.FullPath);

                while (IsFileLocked(fileInfo))
                {
                    attempts++;
                    CreateUploadLog(UploadLogPath, string.Format("Info : {0} is locked, trying again. Attempt #{1}.", fileInfo.Name, attempts));
                    if (attempts == 5)
                    {
                        CreateUploadLog(UploadLogPath, string.Format("Error : {0} is locked, could not access file within 5 attempts.", fileInfo.Name));
                        bailiffEventLogger.WriteEntry(string.Format("Error : {0} is locked, could not access file within 5 attempts.", fileInfo.Name), EventLogEntryType.Error);
                        break;
                    }
                    Thread.Sleep(1500);
                }

                bool isSaveSuccess = SaveToTrim(e.FullPath);
                if(isSaveSuccess)
                {
                    DeleteFile(e.FullPath);
                }
                else
                {
                    MoveFileToError(e.FullPath);
                }

                fileInfo = null;
                Dispose();
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(string.Format("Error while saving or deleting file : {0}", ex.Message), EventLogEntryType.Error);
            }
        }

        /// <summary>
        /// Attemps to upload file to content manager.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        bool SaveToTrim(string path)
        {
            string pathFileNoExt = Path.GetFileNameWithoutExtension(path);

            try
            {
                string[] pathArgs = pathFileNoExt.Split(new string[] { "_" }, StringSplitOptions.RemoveEmptyEntries);
                // Note for stack overflow: These classes and methods are part of an SDK provided by a 3rd party that I'm using to upload documents 
                // into their content management system. I'm not sure, but I don't think the memory leak is occuring at this part.
                
                using (Database dbCntMgr = new Database { Id = ContentManagerEnviro })
                {
                    
                    // Connect to the content manager database.
                    try
                    {
                        dbCntMgr.Connect();
                    }
                    catch (Exception ex)
                    {
                        bailiffEventLogger.WriteEntry(ex.Message, EventLogEntryType.Error);
                        CreateUploadLog(UploadLogPath, "Failed to connect to content manager.");
                        return false;
                    }

                    // Create the record based on record type, set default assignee and default bailiff type.
                    RecordType oRecordType = new RecordType(dbCntMgr, "Revenues - Bailiff");
                    Record oRecord = new Record(dbCntMgr, oRecordType);
                    oRecord.Assignee = new Location(dbCntMgr, "Bailiff Returns Pending");
                    oRecord.SetFieldValue(new FieldDefinition(dbCntMgr, "Bailiff Type"), new UserFieldValue("Liability Order Return"));

                    // Set the default container, not changed if no result is found.
                    Record oContainer;
                    oContainer = new Record(dbCntMgr, "014/1065/0973/55198");

                    // Search via property ID and "80" for Revenues Property File
                    TrimMainObjectSearch search = new TrimMainObjectSearch(dbCntMgr, BaseObjectTypes.Record);
                    TrimSearchClause trimSearchClause = new TrimSearchClause(dbCntMgr, BaseObjectTypes.Record, new FieldDefinition(dbCntMgr, "Property ID"));
                    trimSearchClause.SetCriteriaFromString(pathArgs[2].Substring(2));
                    search.AddSearchClause(trimSearchClause);
                    trimSearchClause = new TrimSearchClause(dbCntMgr, BaseObjectTypes.Record, SearchClauseIds.RecordType);
                    trimSearchClause.SetCriteriaFromString("80");
                    search.AddSearchClause(trimSearchClause);

                    // Sets the container to found record if any are found.
                    foreach (Record record in search)
                    {
                        //oContainer = new Record(dbCntMgr, record.Uri);
                        oContainer = record;
                    }

                    // Once container is finalised, set record container to located container.
                    oRecord.Container = oContainer;

                    // Set the title to name
                    oRecord.Title = pathArgs[3];

                    // Set the input document.
                    InputDocument oInputDocument = new InputDocument();
                    oInputDocument.SetAsFile(path);
                    oRecord.SetDocument(oInputDocument, false, false, "Created Via Bailiff Content Manager Uploader service.");

                    // Save if valid, print error if not.
                    if (oRecord.Verify(false))
                    {
                        oRecord.Save();
                        CreateUploadLog(UploadLogPath, string.Format("File uploaded : {0}", Path.GetFileNameWithoutExtension(path)));
                        return true;
                    }
                    else
                    {
                        CreateUploadLog(UploadLogPath, string.Format("Upload of {0} file attempt did not meet validation criteria. Not uploaded.", Path.GetFileNameWithoutExtension(path)));
                        return false;
                    }
                }
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(ex.Message, EventLogEntryType.Error);
                return false;
            }
        }

        /// <summary>
        /// Deletes file when successfully uploaded.
        /// </summary>
        /// <param name="path"></param>
        void DeleteFile(string path)
        {
            try
            {
                string pathFileNoExt = Path.GetFileNameWithoutExtension(path);

                // If file exists, delete.
                if (File.Exists(path))
                {
                    File.Delete(path);
                    CreateUploadLog(UploadLogPath, string.Format("File deleted from Upload folder : {0}", pathFileNoExt));
                }
                else
                {
                    CreateUploadLog(UploadLogPath, string.Format("Error deleting file from upload folder : {0}", pathFileNoExt));
                }
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(ex.Message, EventLogEntryType.Warning);
                CreateUploadLog(UploadLogPath, ex.Message);
            }
        }

        /// <summary>
        /// Moves non uploaded files (failed to upload) to an error location as specified in the app config.
        /// </summary>
        /// <param name="path"></param>
        void MoveFileToError(string path)
        {
            try
            {
                string pathFileNoExt = Path.GetFileName(path);

                // If directory and file exist, attempt move.
                if(Directory.Exists(UploadErrorLocation))
                {
                    if(File.Exists(path))
                    {
                        if(File.Exists(Path.Combine(UploadErrorLocation, pathFileNoExt)))
                        {
                            File.Delete(Path.Combine(UploadErrorLocation, pathFileNoExt));
                        }

                        File.Move(path, Path.Combine(UploadErrorLocation, pathFileNoExt));
                    } else
                    {
                        CreateUploadLog(UploadLogPath, "Could not move non-uploaded file to error location");
                    }
                } else
                {
                    CreateUploadLog(UploadLogPath, "Could not move non-uploaded file to error location, does the error folder exist?");
                }

            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry("Error while moving file to error location : " + ex.Message, EventLogEntryType.Warning);
                CreateUploadLog(UploadLogPath, ex.Message);
            }
        }

        /// <summary>
        /// Takes full path of upload log path and a message to add to the upload log. Upload log location is specified in the app config.
        /// </summary>
        /// <param name="fullPath"></param>
        /// <param name="message"></param>
        private void CreateUploadLog(string fullPath, string message)
        {
            try
            {
                //StreamWriter streamWriter;

                // If file does not exist, create.
                if (!File.Exists(Path.Combine(fullPath, "UploadLog_" + DateTime.Now.ToString("ddMMyyyy") + ".txt")))
                {
                    using (StreamWriter streamWriter = File.CreateText(Path.Combine(fullPath, "UploadLog_" + DateTime.Now.ToString("ddMMyyyy") + ".txt")))
                    {
                        streamWriter.Close();
                    }
                }
                // Append text to file.
                using (StreamWriter streamWriter = File.AppendText(Path.Combine(fullPath, "UploadLog_" + DateTime.Now.ToString("ddMMyyyy") + ".txt")))
                {
                    streamWriter.WriteLine(string.Format("{0} -- {1}", DateTime.Now.ToString(), message));
                    streamWriter.Close();
                }
            }
            catch (Exception ex)
            {
                bailiffEventLogger.WriteEntry(ex.Message, EventLogEntryType.Warning);
            }
        }

        /// <summary>
        /// Attempts to access the file, returns true if file is locked, false if file is not locked.
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        private bool IsFileLocked(FileInfo file)
        {
            try
            {
                using(FileStream stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None))
                {
                    stream.Close();
                }
            }
            catch(IOException)
            {
                return true;
            }

            return false;
        }
    }
}

所以,我认为有几个方面可能会导致泄漏:

  1. 事件注册/引发——通过几次搜索和一两个小时的实验,我怀疑是事件引发和委托导致了内存泄漏。与完成后不取消注册或处置事件有关吗?
  2. 执行交互/上传的第 3 方 SDK 包含泄漏 - 这可能是可能的,如果回复认为这是原因,那么我将与 SDK 的维护人员一起追查此问题。我有点确信这不是泄漏的原因,因为 SDK 包含一个调试模式,可以通过将任何未处理的对象输出到事件日志的方法启用,尝试过,它没有显示任何对象不被处置。
  3. 上传日志文件写入 - 使用上传事件等写入文件的过程可能是原因吗?我对这是内存泄漏没有多大信心,因为using语句正在用于流写入器,但也许?

你觉得呢?你有没有什么想法?

标签: c#memory-leakswindows-servicesfile-watcher

解决方案


推荐阅读