WinSW实战:把任意exe包装成Windows服务,实现开机自启与自动重启
简介WinSW是一套开源的Windows服务包装工具面向开发人员和系统管理员可将Java、.NET及自定义可执行程序快速封装为Windows服务进而获得后台稳定运行、自动启停、失败重启与统一监控和集中管理等能力。压缩包共含3个文件其中两个exe分别对应64位和32位Windows系统架构一个xml为典型配置文件模板用于指定服务名称、执行命令、启动参数、日志记录方式等关键信息整体大小11.2MB。已有548人学习下载适合需要将后台任务、定时脚本或统一服务化部署的实战场景。借助这套资源读者能直接获得可执行程序与现成配置模板免去从零实现服务封装的成本通过参考xml示例还可快速掌握配置项含义和常见排错思路为后续服务上线、日志分析及性能监控提供扎实基础整体结构清晰易用。1. 把脚本变成“开机自启、挂了自动拉起”的Windows服务其实就靠这三个文件某项目的服务端工具平时靠命令行窗口运行窗口一关进程就跟着没了用任务计划程序做开机启动又总因为用户未登录而掉链子。后来把这套东西包装成 Windows 服务用到的就是标题里的 WinSW-x64.exe、WinSW-x86.exe、sample-minimal.xml 这三个文件。WinSW 是 Windows Service Wrapper 的缩写作用是把任意可执行文件包装成由服务控制管理器SCM统一管理的标准 Windows 服务。适合它的典型人群很明确运维负责的服务端工具、桌面软件里的常驻组件以及那些只有 exe、没有安装向导的“野程序”。只要一个包装器可执行文件加一份 xml 描述就能把程序注册成服务实现开机自启、状态查询、异常退出自动拉起全程不需要改业务代码。2. WinSW 工作原理与 x64、x86 选型别把架构选成玄学2.1 包装器是如何“骗过”服务控制管理器的Windows 服务不是“管理员能启动的后台程序”它必须经过服务控制管理器SCM的注册、校验和生命周期管理。安装服务要在系统里登记服务名与启动信息启动时 SCM 会创建进程并等待其报告“已就绪”停止时 SCM 发送停止信号要求进程正确处理退出。普通可执行文件这套流程全都不懂所以直接双击能正常显示界面的程序一旦注册成服务就可能出现“启动后立即停止”的怪象。WinSW 的方案是把自己变成一个“服务宿主”。它内部实现了与 SCM 握手的完整逻辑安装时向系统注册服务启动时先由 SCM 拉起来再读取同名 xml 配置用创建进程的方式拉起业务子进程子进程创建成功WinSW 向 SCM 报告运行状态子进程退出WinSW 按配置选择重启业务进程或结束自身状态并把退出码回报给 SCM。整套过程对业务程序完全透明业务 exe 不需要感知服务环境。如果只讲这一层很多人会把 WinSW 理解成“一个拉皮筋的启动器”其实它还有一个重要职责是看守。看守的意思是当业务进程因为异常原因退出时WinSW 不会无动于衷。只要 xml 里配了 onfailure 重启指令它会在指定延时后重新创建进程如果没有配置它会跟随退出并向 SCM 报告服务已停止。正是这层逻辑让服务具备了“自动拉起”的能力这也是生产环境里把程序交给服务管理器托管的最大价值。这里有一个判断服务的细节值得新手记住服务的 RUNNING 状态只代表 WinSW 已经成功创建了业务进程并不保证业务代码已经完成初始化。如果你在业务程序加载配置的耗时阶段去查状态看到的更可能是 START_PENDING而业务代码里的初始化错误只能通过日志暴露。所以判断服务是否正常不能只看 SCM 状态要把状态和业务日志放在一起看。这也是后面专门讲日志配置的原因。2.2 x86 与 x64 的选型按系统位数选别按业务进程位数猜标题里的两个包装器文件只是位数不同功能没有差别。选型原则很简单32 位 Windows 用 WinSW-x86.exe64 位 Windows 用 WinSW-x64.exe。查询系统位数最直接的方法是在 cmd 里执行echo %PROCESSOR_ARCHITECTURE%看到 AMD64 就是 64 位看到 x86 就是 32 位。但为什么不能把 WinSW-x86.exe 用在 64 位系统上单从“能不能运行”看可以因为 Windows 内置了 WOW64 兼容层。问题出在兼容层的副作用32 位进程访问文件系统和注册表时会被重定向到 SysWOW64 对应的视图。一个典型场景是业务脚本需要读取C:\Windows\System32\drivers\etc\hosts用 x86 包装器启动时实际可能读的是重定向后的SysWOW64\drivers\etc\hosts文件路径看起来存在内容却对不上。这类问题表现得很玄学同一段脚本换一台机器结果不同排查成本远高于一开始选对版本。还有一层容易纠结的选择业务程序本身是 32 位但操作系统是 64 位选哪个包装器我一般仍然选 x64。因为包装器位数不要求与子进程位数一致64 位包装器完全有能力创建 32 位子进程而包装器自身运行在 64 位模式下能避开路径重定向问题业务进程的工作目录、临时目录、系统工具调用都更接近管理员在命令行手动运行时的行为。反过来如果你的部署环境只有 32 位系统那就只能配 x86在 32 位系统上不要试图用 x64 文件系统根本不会加载它。还有一个容易被忽略的选择依据日志和监控工具。一些日志采集组件在采集 32 位进程输出时会有额外限制或者运行时行为与 64 位不同既然包装器本身是常驻服务让它运行在原生 64 位模式下能给采集、调试和权限控制省去不少麻烦。我见过不止一次因为业务程序是 32 位就选了 x86 包装器结果在日志采集上折腾半天的情况最后换回 x64 就安静了。2.3 同名文件机制exe 和 xml 是怎么绑定在一起的把 sample-minimal.xml 和 WinSW-x64.exe 放在一起说是因为二者之间有一个约定WinSW 启动时会查找“自己的文件名 .xml”作为配置文件。也就是说如果你把 WinSW-x64.exe 改名为 app.exe配置文件就必须叫 app.xml如果不改名保留 WinSW-x64.exe那配置文件就应该叫 WinSW-x64.xml。这个约定决定了部署时的文件组织方式。这个机制的另一个用途是支持多服务共存。需要在一台机器上注册两个不同服务时可以把同一份 WinSW-x64.exe 复制成 broker.exe 和 worker.exe再分别配上 broker.xml 与 worker.xml两个服务完全独立。SCM 里显示哪个服务名取决于 xml 里的 id 节点与文件名无关但 xml 文件找不到时安装和启动都会失败错误提示多半围绕“找不到配置文件”。所以每次动手前第一步永远是确认目录下 exe 和 xml 的文件名前缀完全一致再谈改配置。实际部署时我维护的服务目录通常长这样每个服务一个文件夹里面只有一份包装器副本、一份 xml 和业务程序目录从不把所有服务共用一个 WinSW-x64.exe。共用文件虽然省一点磁盘但会导致 A 服务改配置时影响 B 服务排查时连 xml 归属都分不清。与其纠结配置管理方案不如一开始就把每个服务隔离成独立目录这是十次部署里最能减少返工的一个小习惯。3. 把 sample-minimal.xml 改造成能上生产的配置每个节点都说清3.1 最小配置到底写了什么先看一份 sample-minimal.xml 最精简的内容和该模板默认的结构保持一致仅省略注释service idmyapp/id nameMy App/name descriptionMy App Service/description executablepath/to/application.exe/executable arguments/arguments logmoderotate/logmode /service六个节点按功能分成两组。第一组是服务身份id 是服务在系统里的唯一标识sc query和注册表里都用它不能有空格name 是服务管理器图形界面里显示的名称description 是服务属性的说明文字。第二组是行为描述executable 告诉 WinSW 要拉起哪个程序arguments 是传给目标程序的所有参数logmode 决定目标程序写到 stdout 和 stderr 的内容如何处理。用这份配置装出来的服务只是“刚好能跑”。它能完成注册、启动、停止但不包含开机自启、失败重启、工作目录这些生产环境必备信息。很多人只改 executable 就去 install结果服务装上了、启动也成功业务逻辑却不对多数是缺少后面要讲的几个节点。注意 executable 里的路径写法我建议一律使用绝对路径。服务启动时的工作目录由 workingdirectory 决定默认情况下是C:\Windows\System32xml 所在目录并不参与路径解析用相对路径写的 executable很容易在服务环境下解析到奇怪的地方。arguments 里带空格的路径也需要用引号包裹如果参数里有或这类 XML 特殊字符要写成amp;或lt;否则 XML 解析会失败。3.2 从最小到可用生产配置需要补上这 6 个节点生产环境里我会在最小配置基础上补充以下节点节点作用建议值说明startmode服务启动方式Automatic可选 Automatic、Manual、Delayedworkingdirectory子进程的工作目录程序包绝对路径不设置时默认是 System32logpath日志文件目录独立日志目录和程序目录分开更新程序时不误删日志onfailure进程异常退出后的动作actionrestart delay10000异常退出后延时重启stoptimeout停止超时30 sec防止业务进程不响应导致停止卡住env注入环境变量按需用 name 和 value 成对设置一份我常用的完整配置长这样service idmyapp/id nameMy App/name descriptionMy App Service/description executableD:\apps\myapp\myapp.exe/executable arguments--config D:\apps\myapp\config\app.ini/arguments workingdirectoryD:\apps\myapp/workingdirectory logpathD:\logs\myapp/logpath logmoderotate/logmode startmodeAutomatic/startmode onfailure actionrestart delay10000/ stoptimeout30 sec/stoptimeout env nameAPP_ENV valueproduction/ /service这里逐项解释选择理由。startmode 设置成 Automatic服务会在系统启动时自动拉起如果你希望系统启动完成、桌面就绪后再拉取可以用 Delayed能错开开机高峰但延迟启动意味着服务在开机初期一段时间内不可用需要业务能容忍。workingdirectory 必须显式设置因为默认工作目录是 System32业务程序用相对路径读文件时会出现与命令行完全不同的行为。logpath 单独放到 D:\logs 而不是程序目录下是为了以后更新程序包时不会误删日志也方便日志采集统一挂载。onfailure 是最值得花时间的节点。delay10000表示子进程异常退出后等 10 秒再拉起这个值的单位是毫秒也可以写成10 sec。我一般把 delay 设在 5 到 20 秒之间太短依赖外部资源尚未恢复重启也是白重启太长业务中断时间变长。stoptimeout 设成 30 秒给业务进程足够时间清理资源如果你的业务程序有自定义的退出流程可以把时间放宽到 60 秒但不要不设否则服务停止时如果进程没响应SCM 会一直等待。3.3 日志两级wrapper 日志与业务日志分开看WinSW 的日志分两层。第一层是 wrapper 日志记录服务注册、启动、停止、子进程退出等事件由 WinSW 自己生成第二层是业务日志指的是目标程序通过 stdout 和 stderr 输出的内容由 logmode 控制。这两层日志互不干扰排错时先看 wrapper 日志确认服务生命周期再进业务日志找业务异常顺序不能反否则很容易被误导。logmode 常用 rotate 和 append 两个值rotate 按大小或时间滚动日志避免单个文件无限增大适合长期运行append 把输出全部续写到同一个文件适合短时间调试。我生产上用 rotate调试期临时切成 append 能更方便地追踪全链路但记得调试完改回来。业务程序写日志越频繁越要保留 rotate 模式日志目录和保留策略在部署前想清楚不要等磁盘被写满再救。这个习惯是排查问题时的后悔药也是排障效率的分水岭。4. 从 install 到 uninstall完整命令行流程与验证方法4.1 安装前准备改名、建目录、约定路径动手装服务之前先把文件准备做好能躲掉一半的坑。在一台干净的机器上建议这样组织建立独立服务目录D:\services\myapp不和其他项目混放把 WinSW-x64.exe 复制为D:\services\myapp\myapp.exe把 sample-minimal.xml 另存为D:\services\myapp\myapp.xml业务程序放在D:\apps\myapp\下日志目录单独建在D:\logs\myapp\。注意这里有两个“另存为”的细节。第一个是 xml 不要直接在原始模板文件上改保留一份干净模板方便以后创建其他服务第二个是文件名前缀必须与 exe 完全一致myapp.exe 对应 myapp.xml不允许出现 myapp.exe 配 app.xml 的情况。目录结构确定后xml 里的 executable、workingdirectory、logpath 都按这个结构写绝对路径。如果你打算在一台机器上跑多个服务强烈建议每一个服务都用独立的目录和独立的包装器副本。不要共用同一个 WinSW-x64.exe 然后用不同 xml 区分因为 WinSW 查找 xml 是基于自身文件名共用意味着只能有一个配置文件根本无法区分两个服务。4.2 四组核心命令install、start、stop、uninstall文件就绪后右键以管理员身份打开 cmd 或 PowerShell 窗口进入服务目录执行安装cd /d D:\services\myapp myapp.exe installinstall 的核心动作是向 SCM 注册服务相当于把 xml 里的服务信息写进系统注册表。命令执行成功后服务管理器中会立刻出现名为 My App 的服务条目但初始状态是已停止。判断是否注册成功更可靠的方式是执行查询sc query myappmyapp 对应的是 xml 里的 id不是显示名称。回显中 STATE 一列如果为 SERVICE_STOPPED说明注册成功。接下来启动服务myapp.exe startstart 命令通知 SCM 启动服务WinSW 随后读取 xml、拉起业务子进程。等两秒后再次执行sc query myapp状态会变成 SERVICE_RUNNING同时业务进程开始运行。如果长期停在 START_PENDING 或直接回到 STOPPED说明启动阶段出了问题排查思路见下一章。停止服务有两种等价写法myapp.exe stop net stop myapp两种方式底层都是向 SCM 发送停止请求区别只在于调用入口。卸载服务推荐先停止再卸载myapp.exe uninstall按 stop 后 uninstall 的顺序执行可以让服务状态和注册表保持一致避免留下异常残留。卸载后再次执行sc query myapp会提示服务不存在这就说明删除干净了。4.3 验证服务状态不要只盯着服务管理器的图形界面很多人习惯打开 services.msc 看服务状态其实命令行验证更可靠因为在脚本化运维时图形界面没法嵌入流程。我常用的三条验证命令sc query myapp | findstr STATE sc qc myapp tasklist | findstr myapp第一条看当前状态第二条看服务配置启动方式、可执行路径、依赖关系第三条确认进程列表里是否真的存在名为 myapp 的进程。第三条要特别注意WinSW 启动后任务管理器里会同时出现两个相关进程一个是包装器进程 myapp.exe另一个才是业务进程不少新手看到 myapp.exe 在跑就以为服务正常其实业务进程早就退出服务正处在“有看守、无任务”的空转状态。所以验证服务健康要同时看两条信息包装器进程存在且 SCM 状态为 RUNNING并且业务进程确实在清单里。PowerShell 下还能更进一步把 SCM 状态和进程状态放在一条命令里判断if ((Get-Service myapp).Status -eq Running -and (Get-Process myapp -ErrorAction SilentlyContinue)) { Write-Host service ok }这段的逻辑是服务必须在 Running并且包装器进程存在真实业务进程是否存活可以再按进程名复合判断。这种复合验证适合写进发布脚本比人眼盯任务管理器靠谱得多。5. 常见问题与避坑服务装不上、起不来、改了不生效5.1 安装时报配置错误先对文件名再对 xml现象执行myapp.exe install时很快返回失败提示找不到服务配置或配置无效部分情况下服务管理器里还会残留一个无法启动的服务条目。原因最常见是 exe 与 xml 文件名前缀不一致。有人保留了 WinSW-x64.exe 的文件名却把配置文件命名为 myapp.xmlWinSW 启动时去找 WinSW-x64.xml找不到自然失败也有人手写 xml 时把 id 写成了“my app service”这种带空格的字符串服务名非法导致注册被拒。解决第一步查看目录下文件确认 exe 和 xml 名字前缀一致第二步用文本编辑器或浏览器打开 xml确认标签闭合且 id 值合法如果之前注册过同名服务先sc delete myapp清理残留再重装。这三个动作按顺序走基本覆盖了 80% 的安装失败场景。5.2 服务启动几秒后自动停止别盯着状态重启去看业务日志现象myapp.exe install成功myapp.exe start后服务进入 START_PENDING随后回到 STOPPED服务管理器只显示一般性错误没有具体信息。原因业务进程启动后立即退出。WinSW 作为看守进程发现子进程退出后如果没配 onfailure 就会随之结束如果配了 onfailure 重启会看到服务状态在 RUNNING 与 STOPPED 之间循环。业务进程快速退出的常见原因包括相对路径找不到配置文件、端口被占用、依赖的数据库未就绪。解决先在命令行里手动执行一遍 xml 中 executable arguments 组成的完整命令观察它能否持续运行。命令行正常而服务模式不正常重点排查 workingdirectory 和环境变量命令行也闪退则先解决业务程序自身问题。排查过程中临时把 logmode 改成 append可以让后续多次启动的日志追加到同一个文件方便对比退出前的最后几行输出。注意服务状态的 RUNNING 不代表业务初始化完成正式判断至少要看一次业务日志。5.3 业务程序读不到 System32 下的文件架构选错的经典翻车现象同一份配置64 位系统上换用 WinSW-x86.exe 后业务程序报找不到C:\Windows\System32下某个文件或调用的系统工具提示不存在手动双击运行却一切正常。原因32 位包装器运行在 WOW64 层System32 被重定向到 SysWOW64。文件并没有丢失而是路径视图不同。解决把包装器换回 WinSW-x64.exe让服务宿主运行在原生 64 位模式下。如果由于历史遗留必须用 x86可以尝试把涉及系统目录的路径改写为C:\Windows\Sysnative来访问真实的 System32 视图但这种特殊路径会污染配置我不推荐长期使用。架构选型永远是前置动作事发后才改路径属于亡羊补牢。5.4 改了 xml服务行为却没变配置是启动时读的不是改完就生效现象修改了 xml 里的 executable 或 arguments执行myapp.exe start后服务运行的行为还是旧参数甚至启动的还是旧程序。原因WinSW 在服务每次启动时读取 xml而不是在 install 时把配置固化进系统。如果你的服务本来就在运行执行 start 时 SCM 认为服务已经运行、直接忽略请求新的 xml 根本没有被读取。解决先停止服务再启动服务myapp.exe stop myapp.exe start或者直接用net stop myapp net start myapp完成一次重启。修改 executable、arguments、workingdirectory、logmode 这些运行时配置都只需要重启服务不必卸载重装只有修改了 id 节点才需要卸载后重装因为 id 对应的是 SCM 里注册的服务身份改动后旧条目不会自动迁移。5.5 服务运行中却找不到业务进程别把包装器当业务程序现象任务管理器里看到一个名为 myapp 的进程但找不到真正干活的业务程序于是误以为服务是空转或者业务进程被杀了。原因WinSW 和业务进程是两个进程。myapp.exe 是包装器进程业务进程有自己的可执行文件名在任务管理器里只按服务名找进程自然找不到业务程序。解决用命令行按进程名定位tasklist | findstr myapp如果输出里只有 myapp.exe需要进一步确认它的命令行里带的是哪个业务程序可以用以下命令查看wmic process where namemyapp.exe get ProcessId,CommandLine这样能看到包装器实际拉起的子进程命令。如果想看到父子关系可以在任务管理器里按 PID 对照业务进程的父 PID 通常就是包装器进程的 PID。看清这层关系后就不会把两个进程混为一谈。6. 进阶习惯自动重启、日志轮转和一条可靠的验证命令服务装上只是第一步怎么让它长期稳定运行才是重点。三个习惯值得保留失败自动重启、日志轮转、验证命令固定化。失败自动重启用 onfailure 配置前面已经写过。注意 onfailure 只在业务进程异常退出时触发如果业务进程自己把异常吞掉、一直活着WinSW 不会干预。所以 onfailure 解决的是“进程死了没人知道”的问题解决不了“进程活着但不工作”的问题。日志轮转用 logmode 配合 logpath生产环境必须开否则几个月后日志文件可能冲到几个 GB磁盘写满后服务故障反而变成日志引发的二次事故。第三个习惯把验证命令固定下来。我每次部署完都会执行这样一串检查sc query myapp myapp.exe status net start | findstr myappsc query看状态status看包装器的运行结果net start | findstr确认服务在系统服务列表里。三句合起来比打开服务管理器点刷新要直观得多。如果你在写发布脚本还可以把 sc query 的状态字符串作为判断条件连续检查两次并间隔几秒避免在 START_PENDING 阶段误判为失败。最后强调一个我踩过的坑永远不要在生产环境直接改 xml 里的 id 再重启服务。id 是服务身份改了之后 SCM 会认为是一个全新服务旧服务名连同权限设置会残留并脱离控制。需要改身份时老老实实 uninstall 再 install。宁可多花两分钟重装也不要留下一台状态混乱的服务器。希望帮到你。本文还有配套的精品资源点击获取