在Windows Server 2019打印服务器添加脚本拦截打印作业存文件
Windows Server 2019虚拟打印机拦截打印请求并提取文件方案
问题背景
在Windows Server 2019打印服务器上搭建虚拟打印机,目标是拦截客户端通过系统打印功能(Ctrl+P)发送的打印请求,将文件保存到磁盘供其他应用使用。当前已完成以下基础配置:
- 添加了可网络访问的虚拟打印机,客户端可在Word等软件中正常选择使用
- 能通过PowerShell脚本监控端口及打印作业
- 发现TCP/IP端口配置窗口支持添加脚本,希望在请求发起阶段完成拦截
核心疑问:
- 是否有成熟的打印作业拦截并提取文件的实现案例?
- Windows Server是否需要额外角色或特殊配置?
- 是否需要安装特定的虚拟打印机驱动?
- 自定义打印处理器的作用及实现逻辑是什么?
- 现有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
相关产品推荐
相关产品推荐

