You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Windows Server 2019打印服务器添加脚本拦截打印作业存文件

Windows Server 2019虚拟打印机拦截打印请求并提取文件方案

问题背景

在Windows Server 2019打印服务器上搭建虚拟打印机,目标是拦截客户端通过系统打印功能(Ctrl+P)发送的打印请求,将文件保存到磁盘供其他应用使用。当前已完成以下基础配置:

  • 添加了可网络访问的虚拟打印机,客户端可在Word等软件中正常选择使用
  • 能通过PowerShell脚本监控端口及打印作业
  • 发现TCP/IP端口配置窗口支持添加脚本,希望在请求发起阶段完成拦截

核心疑问:

  1. 是否有成熟的打印作业拦截并提取文件的实现案例?
  2. Windows Server是否需要额外角色或特殊配置?
  3. 是否需要安装特定的虚拟打印机驱动?
  4. 自定义打印处理器的作用及实现逻辑是什么?
  5. 现有C++监控程序仅能检测打印请求,无法提取实际文件,如何修复?

解答

1. 类似实现的可行性

这类需求非常普遍,企业和开发者已有成熟实现,核心思路分为两类:

  • 端口/处理器级拦截:在打印作业提交到驱动前直接拦截原始数据
  • API级监控提取:通过Windows打印服务API捕获作业并导出数据

2. Windows Server角色与配置要求

无需额外添加服务器角色,但需调整以下配置:

  • 确保打印服务器已启用打印服务角色(默认已安装,未安装可通过服务器管理器添加)
  • 给操作账户分配PRINTER_ACCESS_ADMINISTER权限,才能读取打印作业的原始数据
  • 若要在请求发起阶段拦截,可在TCP/IP端口属性的「高级」选项卡中,将「打印处理器」配置为自定义脚本或程序,但该方式仅支持处理RAW格式数据

3. 虚拟打印机驱动选择

推荐两类驱动:

  • 通用RAW驱动:如「Generic / Text Only」或「Microsoft XPS Document Writer」,这类驱动会将打印数据以RAW或XPS格式输出,便于提取
  • 专用虚拟驱动:若需要导出PDF等特定格式,可使用开源或商业虚拟PDF驱动,但需确保驱动支持将打印数据保存到磁盘而非发送到物理设备

4. 自定义打印处理器说明

打印处理器是Windows打印服务的核心组件,负责将应用提交的打印数据转换为打印机可识别的格式。自定义打印处理器可以:

  • 直接拦截打印作业的原始数据
  • 将数据保存到磁盘,无需传递给物理打印机
  • 支持处理RAW、EMF、XPS等多种数据类型

实现自定义打印处理器需使用Windows打印服务SDK,编写符合PrintProcessor接口的DLL,注册到系统后关联到目标虚拟打印机。

5. C++监控程序的修复方案

现有代码无法提取文件的核心问题是ReadPrinter调用逻辑错误,以及作业枚举逻辑不完善。修复后的代码如下:

#include <windows.h>
#include <stdio.h>
#include <winspool.h>
#include <string.h>

#pragma comment(lib, "winspool.lib")

#define MAX_PRINTER_NAME 256
#define MAX_JOB_NAME 256

// 确保保存目录存在
void EnsureDirectoryExists(const char* path) {
    char dir[MAX_PATH];
    strcpy_s(dir, path);
    for (char* p = strchr(dir + 1, '\\'); p; p = strchr(p + 1, '\\')) {
        *p = '\0';
        CreateDirectoryA(dir, NULL);
        *p = '\\';
    }
    CreateDirectoryA(dir, NULL);
}

void SavePrintJobData(HANDLE hPrinter, JOB_INFO_2* pJobInfo) {
    DWORD dwNeeded = 0, dwReturned = 0;
    HANDLE hJob = NULL;
    DOC_INFO_1 docInfo;

    // 重新打开打印机以获取足够权限
    HANDLE hTargetPrinter = NULL;
    if (!OpenPrinterA(pJobInfo->pPrinterName, &hTargetPrinter, NULL)) {
        printf("打开打印机失败,错误码: %d\n", GetLastError());
        return;
    }

    docInfo.pDocName = pJobInfo->pDocument;
    docInfo.pOutputFile = NULL;
    docInfo.pDatatype = pJobInfo->pDatatype;

    hJob = StartDocPrinter(hTargetPrinter, 1, (LPBYTE)&docInfo);
    if (hJob == INVALID_HANDLE_VALUE) {
        printf("启动文档失败,错误码: %d\n", GetLastError());
        ClosePrinter(hTargetPrinter);
        return;
    }

    if (StartPagePrinter(hJob)) {
        BYTE buffer[4096];
        DWORD bytesRead;
        char filename[MAX_PATH];
        sprintf_s(filename, sizeof(filename), "C:\\PrintJobs\\%s_%d.prn", pJobInfo->pDocument, pJobInfo->JobId);
        
        EnsureDirectoryExists("C:\\PrintJobs");

        FILE* file;
        if (fopen_s(&file, filename, "wb") == 0) {
            while (ReadPrinter(hJob, buffer, sizeof(buffer), &bytesRead) && bytesRead > 0) {
                fwrite(buffer, 1, bytesRead, file);
            }
            printf("作业数据已保存到: %s\n", filename);
            fclose(file);
        } else {
            printf("无法打开文件: %s\n", filename);
        }
        EndPagePrinter(hJob);
    } else {
        printf("启动页面失败,错误码: %d\n", GetLastError());
    }

    EndDocPrinter(hJob);
    ClosePrinter(hTargetPrinter);
}

void MonitorPrintJobs() {
    PRINTER_NOTIFY_INFO* pInfo = NULL;
    PRINTER_NOTIFY_OPTIONS options;
    HANDLE hChange = NULL;
    DWORD dwChange;

    PRINTER_NOTIFY_OPTIONS_TYPE types[1];
    types[0].Type = JOB_NOTIFY_TYPE;
    types[0].Count = 3;
    types[0].pFields = (PWORD)malloc(3 * sizeof(WORD));
    types[0].pFields[0] = JOB_NOTIFY_FIELD_JOB_ID;
    types[0].pFields[1] = JOB_NOTIFY_FIELD_DOCUMENT;
    types[0].pFields[2] = JOB_NOTIFY_FIELD_STATUS;

    options.Version = 2;
    options.Flags = PRINTER_NOTIFY_OPTIONS_REFRESH;
    options.Count = 1;
    options.pTypes = types;

    // 替换为你的虚拟打印机名称
    const char* printerName = "你的虚拟打印机名称";
    HANDLE hPrinter = NULL;
    PRINTER_DEFAULTS defaults = { NULL, NULL, PRINTER_ACCESS_ADMINISTER };

    if (OpenPrinterA((LPSTR)printerName, &hPrinter, &defaults)) {
        hChange = FindFirstPrinterChangeNotification(hPrinter, PRINTER_CHANGE_JOB_ADDED, 0, &options);

        if (hChange != INVALID_HANDLE_VALUE) {
            printf("开始监控打印作业...\n");
            while (TRUE) {
                dwChange = WaitForSingleObject(hChange, INFINITE);
                if (dwChange == WAIT_OBJECT_0) {
                    DWORD dwResult;
                    if (FindNextPrinterChangeNotification(hChange, &dwResult, &options, (LPVOID*)&pInfo)) {
                        if (pInfo && pInfo->Count > 0) {
                            // 枚举所有未完成的作业
                            DWORD cbNeeded = 0, cReturned = 0;
                            EnumJobs(hPrinter, 0, 100, 2, NULL, 0, &cbNeeded, &cReturned);
                            if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
                                JOB_INFO_2* pJobs = (JOB_INFO_2*)malloc(cbNeeded);
                                if (EnumJobs(hPrinter, 0, 100, 2, (LPBYTE)pJobs, cbNeeded, &cbNeeded, &cReturned)) {
                                    for (DWORD i = 0; i < cReturned; i++) {
                                        // 仅处理新添加的作业
                                        if (pJobs[i].Status & JOB_STATUS_SPOOLING) {
                                            SavePrintJobData(hPrinter, &pJobs[i]);
                                            // 标记作业为已完成,避免重复处理
                                            SetJob(hPrinter, pJobs[i].JobId, 0, (LPBYTE)&pJobs[i], JOB_CONTROL_COMPLETE);
                                        }
                                    }
                                }
                                free(pJobs);
                            }
                        }
                        if (pInfo) {
                            FreePrinterNotifyInfo(pInfo);
                            pInfo = NULL;
                        }
                    }
                }
            }
            FindClosePrinterChangeNotification(hChange);
        } else {
            printf("创建通知失败,错误码: %d\n", GetLastError());
        }
        ClosePrinter(hPrinter);
    } else {
        printf("打开打印机失败,错误码: %d\n", GetLastError());
    }

    free(types[0].pFields);
}

int main() {
    MonitorPrintJobs();
    return 0;
}

修复要点:

  • 修正ReadPrinter调用逻辑:需先通过StartDocPrinter和StartPagePrinter获取作业句柄,才能读取原始数据
  • 添加目录自动创建逻辑,确保保存路径存在
  • 调整作业枚举逻辑,仅处理处于SPOOLING状态的新作业,避免重复处理
  • 使用PRINTER_ACCESS_ADMINISTER权限打开打印机,确保能读取作业数据
  • 文件名添加作业ID后缀,避免重名冲突

内容的提问来源于stack exchange,提问作者Obalony Pivvot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 14:07:34