65.9K
CodeProject 正在变化。 阅读更多。
Home

一个用于管理和创建日志文件的服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (56投票s)

2003年11月16日

CPOL

23分钟阅读

viewsIcon

240440

downloadIcon

8892

一个用于管理和创建日志文件的服务及客户端代码

引言

在我以前的工作中,我曾与一个团队一起开发服务器软件。我们的产品由一套框架和一堆插件DLL组成。在QA调试循环中,很难知道控制线程是在框架可执行文件中,还是已经传递给DLL。知道它在哪里很重要,因为这将告诉我们应该找谁沟通。

我们的处理过程以某种形式接收一个任务,并对其应用多种可能的转换之一。每个转换都是一名开发人员的职责,而决定具体应用哪种转换则是另一名开发人员的职责。记录任务在整个过程中的进度是有意义的,并在此过程中捕获选择特定路径的原因。这样我们就能将转换问题隔离到特定的模块。

我承担了编写一个日志系统的任务,该系统将实现以下目标。

  1. 在被记录的应用程序崩溃后仍能幸存。
  2. 最大程度地减少对被记录应用程序的性能影响。这一点特别重要,因为日志系统不会是一个“仅调试”的方案。我们的软件发布版本也会进行日志记录。
  3. 以模块为基础,记录每个开发人员认为相关的事项。
  4. 提供过滤日志文件的能力,使你想要看到的内容从众多的信息中脱颖而出。
  5. 能够“老化”日志文件。
  6. 易于对现有代码进行改造。
很快我就意识到,服务是实现目标1和2的最简单方法。问题在于,如果日志代码是应用程序的一部分,那么要在不影响被记录应用程序性能的情况下保证消息被记录,就意味着无法使用保证“刷新到磁盘”的方式。你要么让文件系统缓存你的输出,在这种情况下,如果应用程序崩溃,你可能会丢失输出;要么你等待操作系统将你的输出刷新到日志文件。将应用程序的日志部分作为单独的进程运行,可以巧妙地避开这种冲突。毕竟,你并不真正关心一条日志记录需要几秒钟才能到达日志文件,只要它能到达,即使你的应用程序已经崩溃。

通过标准化日志文件格式实现了目标3和4。日志文件格式的变化之大令人惊讶。我选择了一种CSV格式的文本文件。日志文件中的每个条目都像这样

timestamp,sequence,numericcode,threadID,modulename,servername,messagestring
messagestring 部分中的逗号不分隔字段。我认为强调日志文件格式是执行日志记录的代码的功能很重要。服务本身仅缓冲日志字符串并将它们写入永久存储。它不会对其处理的数据进行任何解释。

你可以用记事本打开日志文件,并期望能够阅读它。本系列的第2部分将介绍一个能够实时显示和过滤日志文件的查看器。该查看器足够智能,可以处理输出中嵌入的换行符/新行。这使得包含例如ODBC驱动程序中的错误消息变得容易,而无需对数据进行任何处理,如果你不想减慢应用程序的速度,这是一个重要的考虑因素。

通过在服务中添加一个线程来自动滚动日志文件名和清理旧日志文件,实现了目标5。

目标6?我想我已经实现了。你来评判吧。

为什么不使用 Windows 事件日志?

我决定不使用 Windows 事件日志,主要是因为(正如 John M. Drescher 在下面指出的那样)使用日志系统进行调试会话可能会生成数百兆字节的日志数据,远远超出事件日志设计处理的数据量。此外,重要的是要记住,Windows 事件日志是一种共享资源,由所有软件(包括操作系统本身)共享。我认为将系统日志数据与应用程序日志数据分开很重要,特别是当大部分数据可能是调试数据时。

系统结构

日志系统由三部分组成。第一部分是日志服务本身,本文将介绍。

第二部分是客户端代码,它被编译到被记录的应用程序中,本文也将介绍。

第三部分是日志文件查看器,将在本系列的第二部分中介绍。

日志服务

logger 服务启动时,它会立即生成一个监听线程,该线程打开一个名为 \\.\pipe\logger 的命名管道。客户端连接到该管道并发送一个命令字符串,例如 createlog,myproduct。服务会检查是否已控制着一个同名的日志文件;如果是,它会创建一个名为 \\.\pipe\myproduct.log 的新命名管道实例,并将该管道连接到日志文件。但是,如果服务尚未拥有此名称的日志文件,它会创建一个 CLogInstance 对象,创建一个命名管道并将其连接。ClogInstance 对象反过来创建一个名为 myproduct.log 的磁盘文件,并将写入命名管道的任何内容复制到该文件中。

监听线程通过 CPipeData 对象管理所有连接和日志文件。新创建的 CLogInstance 对象以日志文件名作为键保存在一个映射中,新创建的 CServerPipeInstance 对象存储在一个数组中。每个 CServerPipeInstance 对象都连接到一个现有的 CLogInstance 对象。服务中始终只有一个 CPipeData 实例。

CPipeData 对象

class CPipeData : public CObject
{
    DECLARE_DYNAMIC(CPipeData);
public:
                CPipeData();
    virtual     ~CPipeData();

    void        AddPipe(CServerPipeInstance *pPipe);
    void        RemovePipe(DWORD wIndex);
    void        RotatePipes(DWORD wIndex);

    CServerPipeInstance *GetPipe(DWORD wIndex)
        { return (CServerPipeInstance *) m_pipeArray.GetAt(wIndex); }

    DWORD        GetCount() const
        { return DWORD(m_handleArray.GetSize()); }

    DWORD        Wait()
        { return DWORD(WaitForMultipleObjects(m_handleArray.GetSize(),
                           m_handleArray.GetData(), FALSE, INFINITE));
        }

private:
    CObArray        m_pipeArray;
    CArray<HANDLE, HANDLE>m_handleArray;
};
这里重要的是 m_pipeArraym_handleArray 成员变量和 Wait() 函数。线程通过使用重叠 I/O 创建一个命名管道开始,该管道监听连接请求。当连接请求到达时,它们被解析为请求动词(目前只支持 createlog)和日志文件名。然后,线程搜索 m_logMap 以查找匹配的日志文件名。如果未找到匹配项,则创建一个新的 CLogInstance 对象并将其添加到日志映射中。然后,它创建一个匹配的 CServerPipeInstance 对象并将其连接到 CLogInstance 对象(无论是预先存在的还是新创建的)。

每当创建了一个 CServerPipeInstance 对象时,与该命名管道关联的事件句柄(CServerPipeInstance 对象中的 m_op.hEvent)就会添加到事件句柄数组中,该数组用于 WaitForMultipleObjects() 调用。CPipeData 对象还维护一个并行数组,其中包含指向每个 CPipeInstance 对象的指针。句柄数组和管道数组之间存在一对一的对应关系。

当监听线程进入 WaitForMultipleObjects() 调用时,线程会等待,直到数据被写入它正在监控的其中一个管道。发生这种情况时,系统会发出事件句柄信号,并且 WaitForMultipleObjects() 调用会返回一个 DWORD,该 DWORD 指定了发出信号的事件句柄的数组索引。我们知道哪个句柄被发出信号,因此我们知道要操作哪个管道。监听线程从该管道读取数据,并将其写入由该管道指向的 CLogInstance 对象管理的日志文本消息队列的头部。

句柄数组中实际上比日志管道多两个句柄。索引0处的句柄是一个事件句柄,用于在服务想要关闭时向线程发出信号。有关此内容的讨论,请参阅我在重叠I/O的一种用途[^]上的文章。索引1处的句柄是命令命名管道\\.\pipe\logger使用的事件句柄。句柄2到n对应于响应发送到命令管道的createlog命令而打开的命名管道。

因此,监听线程监控的句柄数组将始终包含至少两个句柄,最多可达 MAXIMUM_WAIT_OBJECTS,根据我拥有的 Platform SDK 版本,该值定义为 64。这意味着最多可以通过同一个服务器实例记录 62 个模块。

ClogInstance 对象

class CLogInstance : public CObject
{
public:
                CLogInstance();
                ~CLogInstance();

    BOOL        Create(LPCTSTR szBaseName);
    void        QueueMessage(LPCTSTR szLogEntry);

private:
    static void LogProcessorThread(LPVOID data);
    static void LogAgingThread(LPVOID data);

    CStringList m_messageQueue;
    CMutex      m_mutex;
};
每个 CLogInstance 对象都会运行一个线程来监视日志文本队列并将内容写入永久存储的日志文件。

每个日志文件有一个 CLogInstance 对象,并且有零个到多个 CServerPipeInstance 对象指向同一个 CLogInstance 对象。

日志文件以共享模式打开,允许任何其他进程读取或写入该文件。为了获得最大性能,我们希望文件始终保持打开状态,但我们需要向其他进程提供读取访问权限,以便在服务运行时实际查看日志文件内容。写入访问?有时,能够在不停止服务的情况下删除文件内容是有用的。

请注意,由于日志文件老化,一旦我们创建了 CLogInstance 对象,即使所有连接到该对象的管道都已关闭,我们也绝不会删除它。这只有一个含义。只要服务正在运行,您将无法删除日志文件,直到日志文件老化机制启动。

CServerPipeInstance 对象

class CServerPipeInstance : public CObject
{
public:
                CServerPipeInstance();
                ~CServerPipeInstance();

protected:
    CLogInstance *m_pOwner;

private:
    HANDLE      m_hPipe;
    OVERLAPPED  m_op;
    DWORD       m_dwRead;
    CString     m_csBaseName;
};
这里重要的部分是指向 CLogInstance 对象的指针,它是此管道的所有者,以及 OVERLAPPED 结构。每个 OVERLAPPED 实例都包含一个事件句柄,用于监视该命名管道上的活动。该事件句柄被添加到 CPipeData 对象的 m_handleArray 成员中。

每个命名管道都以消息模式运行,这保证在返回与单个写入对应的完整数据字符串之前,读取不会返回数据。如果管道以字节模式运行,写入日志文件的结果将是一串似乎从所有被监视的管道中随机选择的单个字节,这不是所需的结果。

数据添加到队列后,队列监视线程会收到信号。它从队列头部取出消息并将其写入永久存储。永久存储的基名相同,为 myproduct.log,存储在你想要的任何目录中。本文提供的服务源代码将它们放在一个名为 logger 的目录下,该目录位于你的 %systemroot% 目录下。

当客户端应用程序关闭其管道端时,相同的管道事件句柄被发出信号,导致等待终止。代码使用 GetOverlappedResult() 检查发出信号的句柄的结果,如果操作失败,它使用 GetLastError() 来确定要采取的行动。如果管道损坏,它将从管道数组中移除,其句柄将从句柄数组中移除,并且管道本身将被删除。

应用程序继续愉快地做它该做的事情,并将日志消息推入管道。管道的另一端尽快取出消息并将它们排队,以便另一个线程有机会时将其写入磁盘。你希望尽快清空管道,否则客户端将停滞,直到管道中有新消息的空间。你还希望尽可能地优先考虑磁盘。从某种程度上说,每次写入时都使用文件刷新与此目标相悖,但我们正在平衡对被记录应用程序的性能影响与确保消息确实被记录!

WaitForMultipleObjects() 中的一个陷阱

WaitForMultipleObjects() 的工作方式有一个隐藏的陷阱。作为我的测试的一部分,我编写了一个快速而粗糙的客户端,它向同一个日志文件打开了四个管道,然后依次向每个管道写入 1000 个字符串。也就是说,字符串写入 pipe1,字符串写入 pipe2,依此类推,当 pipe4 写入完成后,再循环回到 pipe1。我期望在日志文件中看到相同顺序的字符串,但没有,我发现了一块块几十个 pipe1 的字符串,然后是一块块几十个 pipe2 的字符串,依此类推。经过调查,我发现 MSDN 确实记载了如果多个事件句柄同时发出信号,WaitForMultipleObjects() 调用会返回编号最小的发出信号的事件句柄的索引。实际上,这非常合理。所以我添加了一个函数 RotatePipes(),用于将刚刚处理过的句柄从其在句柄数组中的位置取出并移到末尾,同时将相同的排序更改应用于 CServerPipeInstance 数组,从而使管道服务以循环方式进行。只有由命令管道创建的管道(管道 2 到 n)才会以这种方式进行洗牌。

在实际场景中,轮询代码可能并非必需(毕竟,测试程序除了尽可能快地写入消息外什么也没做,但这就是压力测试的目的——找出这类情况)。

CLogMap 类

class CLogMap : public CMapStringToOb  
{
public:
                    CLogMap();
    virtual         ~CLogMap();

    CLogInstance    *GetLog(LPCTSTR szLogFileName);

private:
    static void     LogAgerThreadProc(LPVOID data);
    void	        AgeLogFiles();

    HANDLE          m_agingThreadHandle;
};
该类用于维护一个以日志文件名作为键的 CLogInstance 对象映射。它还包含用于老化日志文件的线程(LogAgerThreadProc())。GetLog() 方法返回一个 CLogInstance 对象指针,该指针要么对应于一个预先存在的对象,要么对应于一个新的对象,如果我们尚未控制此日志文件。

日志文件老化

当老化线程启动时,它会计算到本地午夜还有多少秒,然后休眠该时间长度。当它醒来时,它会阻塞队列监视线程,关闭日志文件,将其重命名为 myproduct.yyyymmdd.log,并创建一个新文件以继续日志记录。它还会枚举现有已老化的日志文件并删除早于7天的文件。

选择 myproduct.yyyymmdd.log 格式是为了使老化的日志文件在目录列表中按年龄排序。

我将7天硬编码到这里提供的源代码中。在日志系统的实际应用中,你可能希望能够指定保留日志文件多长时间,但我认为介绍如何做到这一点超出了本文的范围。

日志文件老化陷阱

我遇到了两个陷阱。其中一个是我决定计算从现在到本地午夜的时间并等待该时间段的直接结果。代码看起来像这样。
while (bStop == FALSE)
{
    switch (WaitForSingleObject(hStopEvent, CalculateTimeTillMidnight())
    {
    case WAIT_OBJECT_0:
        bStop = TRUE;
        break;

    case WAIT_TIMEOUT:
        //	Do logfile aging...
        break;
    }
}
这是编写等待期最有效的方法。线程除非收到停止信号或直到超时发生,否则不会运行。这是一件好事。不运行的代码不消耗 CPU 周期,甚至可能在一段合理的时间后从物理内存分页到页面文件中。但这里有一个小问题。如果你想通过将系统时钟调到午夜前一点来测试日志文件老化,你不会看到老化的日志文件突然出现。超时是在等待开始时计算的,一旦开始,就不再引用系统时间。如果你想以这种方式测试日志文件老化,则需要更改系统时间,然后停止并重新启动服务。

另一个陷阱在于 Windows 处理文件重命名和创建新文件时使用重命名文件名称的方式。即,将 myproduct.log 重命名为 myproduct.yymmdd.log,然后立即创建一个名为 myproduct.log 的新文件。Windows 将从文件缓存中提取文件属性信息,包括创建日期。因此,新创建的日志文件将具有原始文件的创建日期,即使它是在最多 24 小时后创建的。我不得不承认这种行为让我感到困惑,但它确实存在...这在 MSDN 中作为 CreateFile() API 的注意事项之一进行了记载。

因此,我们耍了一些花招来设置新创建日志文件的创建日期/时间。

服务器端过滤

不发生。句号。服务将记录写入日志管道的任何内容。它将写入其创建的管道的任何内容原样复制到附加到该管道的日志文件。

客户端代码

这由一个类 CLogger 组成。您可以在每个要执行日志记录的应用程序或模块中将该类的一个实例实例化为全局对象。然后,在您的代码的某个早期位置,您使用模块名称、日志文件名称以及可能的服务器名称(运行日志服务的机器名称)初始化该对象。然后,每次您想要记录某些内容时,您都调用 Log() 方法。Log() 方法接受一个 printf() 样式的格式化字符串和可变数量的参数。
//  At global scope
CLogger gblLogger;

//  Early in your program, maybe in DLLMain or InitInstance
gblLogger.Initialise(3, _T("MyProductModule1"), _T("MyProduct.log"));

//  And then, wherever in your code you want to log something...
gblLog.Log(_T("Format string", arg1, arg2, argn);
如果你愿意,你可以在 CLogger 对象上编写一堆操作符重载,以允许
gblLog.Log << data1 << data2;
但我个人不喜欢那种编写对象的方式,所以我不用那种方式。

Clogger 对象实例需要初始化。我的设计设想(但不是强制要求)该对象是全局的,因此构造函数不带参数,仅将成员变量初始化为安全值。

初始化您的 CLogger 实例

void Initialise(
        DWORD dwLogLevel,
        LPCTSTR szModuleName,
        LPCTSTR szLogFileName,
        LPCTSTR szServerName = _T(".")
);

第一个参数告诉对象所需的日志级别。通常你会从注册表中取出这个值,但为此你需要一种方法首先将该值放入注册表中。与设置老化日志文件的保留期一样,我认为这超出了本文的范围。

第二个参数标识了使用此特定 CLogger 对象实例生成消息的模块。如果您的应用程序由一个可执行文件和一堆 DLL 组成,那么记录哪个模块创建了日志消息真的很有帮助。

第三个参数指定日志文件的名称。每个模块是记录到自己的文件还是应用程序的所有部分都记录到同一个文件,这取决于个人喜好。个人经验是,如果有一种方法可以过滤消息,那么记录到同一个文件会更好。我在本系列第二部分中介绍的 LogFileViewer 提供了该功能。

可选的第四个参数指定日志消息应定向到的机器名称。这允许您将运行在不同机器上的多个应用程序实例记录到一个中央日志服务器。如果未指定,则此第四个参数默认为 .(点),它是应用程序运行所在机器的别名。

每条记录的消息都包含运行该模块的机器名称和调用 Log() 函数的线程的 threadID,这使得区分看似相同但实际上不同的消息变得容易。

如果你检查 Initialise() 函数的代码,你会发现有 20 毫秒的睡眠时间(在单处理器 NT 机器上是两个时间片,在多 CPU 机器上至少是一个时间片)。不要试图删除它。每次调用 Initialise 时,你都在向另一个进程中的命名管道写入。那个其他进程需要时间来完成你要求它做的事情。因此,你的进程需要暂停自己,以便在尝试将数据写入日志之前给服务一个运行的机会。如果你的应用程序不暂停自己,你就有可能尝试写入尚未创建的日志命名管道实例。

为什么要使用命名管道?

我选择使用命名管道作为客户端代码和服务之间的通信机制,因为我希望能够从应用程序中的多个模块写入同一个日志文件。但我也希望不必修改模块之间的 API 以包含日志方法。你愿意为了传递一个日志对象指针而扩展 DLL 公开的每个函数的参数列表吗?我想不会。通过使用命名管道,我可以在不更改模块外部接口的情况下向任何模块添加日志功能。

日志级别

如前所述,CLogger 类需要一个日志级别参数。这为应用程序提供了一种预过滤要记录哪些消息的方法。对此看法不一。一方面,记录所有内容是好的,特别是当查看日志文件时有过滤方法可用时。另一方面,开发人员很容易过分热衷于记录所有内容,这将影响应用程序/系统性能并创建巨大无比的日志文件。(我们前沿的开发人员拥有数百千兆字节的存储空间,但根据我的经验,生产系统在规模上落后于开发人员系统好几个数量级)。

我不想为了额外的日志记录而创建特殊的构建版本。因此有了日志级别。

  • 级别 0 - 错误代码范围为 0 到 999
  • 级别 1 - 警告代码范围为 1000 到 1999
  • 级别 2 - 成功代码范围为 2000 到 2999
  • 级别 3 - 信息代码范围为 3000 到 3999
  • 以此类推,信息代码的级别不断升高
我选择这套定义是基于一种关于应该记录什么的理念。我总是希望能够看到错误代码,因此 CLogger 类将始终报告级别0的消息。除非我真的磁盘空间紧张,否则我可能希望看到警告。成功代码?有些应用程序需要它们(例如,一个创建可审计文档的系统可能希望记录成功和失败)。信息?那是为我们开发人员、质量保证人员以及产品支持需要更详细信息时准备的。您的客户可能不喜欢被要求打开级别17的日志记录并给您发送一张包含500兆日志文件的CD,但如果这能让他们的问题在几天而不是几周内得到解决,他们会很感激。

所以,我们的理念是编写代码来记录所有内容,然后在运行时决定不记录某些内容。将调用(和格式字符串)编译到您的最终产品中会使二进制交付物更大,这可能是可以原谅的。但如果这一切使您的应用程序性能减半,那将不可原谅。Log() 函数的编写旨在执行最少的工作,以确定消息是否甚至会被记录。

void CLogger::Log(DWORD dwErrorCode, LPCTSTR szFormatString, ...)
{
    if (dwErrorCode / 1000 <= m_dwLogLevel)
    {
        ASSERT(szFormatString);
        ASSERT(AfxIsValidString(szFormatString));

        va_list args;

        va_start(args, szFormatString);
        Log(dwErrorCode, szFormatString, args);
        va_end(args);
    }
}
对于消息级别大于日志级别的情况,这是非常少的工作。也就是说,当消息将被丢弃时。

如果服务未运行会怎样?

客户端代码等待 100 毫秒(定义为常量 PIPE_WAIT_TIME),以等待管道可用。如果管道未及时可用,代码将重试多达 10 次(定义为常量 PIPE_RETRY_COUNT),并最终放弃。这意味着您将为每个被记录的模块承担高达 1 秒的开销,这在第一次 Log() 调用时产生。听起来不多,但如果您使用 62 个模块,那将是一分钟的时间。我曾考虑添加代码来启动服务,如果它没有运行,但决定不这样做。这样的代码只有在应用程序以管理员权限运行的情况下才有用。

这些数字,分别是100毫秒和10次,是凭直觉判断的。如果你正在向一个负载很重的机器或通过一个负载很重的网络进行日志记录,你可能需要增加这些数字。

我们尝试 10 次,每次超时 100 毫秒,而不是尝试一次,超时 1 秒,因为每次重试都会尝试确定管道是否存在。如果管道不存在,代码会向命令管道发送管道创建请求并再次尝试。如果我们的服务被应用程序各个部分的消息淹没,这给了它一个机会来赶上并创建一个新的通信通道。

如果您不打算将日志服务作为产品交付的一部分,但又想将其用于开发,请务必包含一些机制(例如 #ifdef _DEBUG)来将日志代码从发布版本中排除。

其他类

此项目中包含三个实用程序类。第一个是 CSleeper 类。它存在的唯一目的是创建一个永不发出信号的事件句柄,并在此句柄上等待指定的时间量。我编写这个类是为了避免可能的死锁。我无法预料 CLogger 类将出现在哪种应用程序中。如果它被编译到使用 DDE 的应用程序中,Sleep() API 可能会变得不安全。请参阅 MSDN 中关于 Sleep() 的文档。

第二个实用程序类是 CSecurityToken 类。它只是创建一个 SECURITY_ATTRIBUTES 结构,并将其初始化为任何使用它创建的系统对象都授予所有人访问权限。危险吗?也许,如果你的代码没有以其他方式检查权限,或者你的代码可以做危险的事情。

最后一个实用程序类是 CClientPipeInstance 类,它代表命名管道的客户端。

测试环境

我在一台双CPU的Windows 2000机器(双核Athlon 2000+)上编写了这段代码,并在(显然是)开发机器和一台单CPU的Windows 2000机器上进行了测试。由于它是多线程的,因此在具有2个物理CPU的机器上进行测试很重要。我还测试了从一台机器到另一台机器的日志记录。

在您的应用程序中使用代码

该服务本身是使用 VC6 服务向导编写的。因此,如果您使用以下命令启动它一次,它支持自注册
logger /service
这会在 Windows 服务控制管理器中将其注册为服务。

客户端代码需要包含以下文件。

sleeper.h
sleeper.cpp
SecurityToken.h
SecurityToken.cpp
clientpipeinstance.h
clientpipeinstance.cpp
logger.h
logger.cpp
这些文件出现在 loggerclient_src.zip 下载中。

安装服务

如果您在同一台机器上构建并运行服务,则无需安装服务,构建过程会自动将其注册到服务控制管理器。在其他机器上,您需要从命令行执行此操作
logger /service
这将把服务注册为手动启动服务。由于客户端代码不会在服务未运行时尝试启动它,因此您需要打开服务控制面板小程序并将服务设置为自动(并重启或启动它),或者修改此行
    SC_HANDLE hService = ::CreateService(
        hSCM, m_szServiceName, m_szServiceName,
        SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
        SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
        szFilePath, NULL, NULL, _T("RPCSS\0"), NULL, NULL);
CServiceModule::Install() 函数中改为
    SC_HANDLE hService = ::CreateService(
        hSCM, m_szServiceName, m_szServiceName,
        SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
        SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
        szFilePath, NULL, NULL, _T("RPCSS\0"), NULL, NULL);

卸载服务

要卸载服务,请从命令行执行此操作。
logger /unregserver

修订历史

  • 2003年11月16日,初始版本。

  • 2003年11月18日。采纳了 Forogar[^] 的一些建议(参见下面的主题帖子)。

    在日志文件老化方法中添加了一些缺失的代码。最初呈现时,每个日志文件的创建日期未设置,因此日志文件老化实际上不会删除旧日志文件。

    重构了部分代码,并创建了一个新类 CLogMap 来托管 CLogInstance 对象并执行日志文件老化。

  • 2003年11月24日。发现并修复了 CLogInstance::LogProcessorThread() 函数中的一个错误。之前呈现的版本未能正确处理其他进程(即正在运行的日志文件查看器)删除文件内容的情况。在新版本中,在将消息写入日志文件之前,我们会寻找到日志文件的当前末尾。

    我改变了生成时间戳的方式,使用了 GetLocalTime() API 而不是 CTime。我不知道我是怎么错过这个 API 的。幸运的是,CodeProject 的专题文章恰好及时地展示了这篇文章[^]。感谢 RK_2000[^]。新的时间戳至少假装提供了毫秒级计时。

  • 2003年11月29日。补充了一些解释性说明,阐明了为何使用命名管道作为服务与客户端代码之间的通信机制。还解释了客户端代码在放弃之前为何会尝试建立连接10次。

  • 2004年12月9日。更新了下载。

  • 2004年1月7日。新增了关于如何使服务自动启动以及如何卸载服务的部分。
© . All rights reserved.