├── .gitignore ├── bin ├── install-all-svc.bat ├── install-all.ps1 ├── log4net.dll ├── log4net.xml ├── register-this-path.bat ├── register-this-path.win10.bat └── svc.exe ├── readme.eng.md ├── readme.md ├── samples ├── csharp-version │ ├── outfiles │ │ └── lastline.log │ ├── svc.conf │ └── worker │ │ └── sample-worker.exe ├── nodejs-version │ ├── outfiles │ │ └── lastline.log │ ├── svc.conf │ └── worker │ │ └── index.js └── python-version │ ├── outfiles │ └── lastline.log │ ├── svc.conf │ └── worker │ └── main.py └── src ├── Conf.cs ├── Consts.cs ├── EasyService.sln ├── Libs.cs ├── Main.cs ├── Main.csproj ├── MyFileLogger.cs ├── SampleWorker.cs ├── SampleWorker.csproj ├── SvcUtils.cs ├── Worker.cs └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | !lastline.log 3 | svc.log 4 | /.vs/ 5 | /src/.vs/ 6 | /src/obj/ 7 | /src/packages/ 8 | -------------------------------------------------------------------------------- /bin/install-all-svc.bat: -------------------------------------------------------------------------------- 1 | @powershell "%~dp0\install-all.ps1" -------------------------------------------------------------------------------- /bin/install-all.ps1: -------------------------------------------------------------------------------- 1 | Write-Host EasyService Batch Install Tool 2 | Write-Host 3 | Write-Host This tool installs all EasyServices in all sub-directories in current directory. 4 | 5 | ForEach($d in (Get-Childitem -Directory)) 6 | { 7 | if (-Not (Test-Path -Path "$d\svc.conf" -PathType Leaf)) { 8 | Write-Host 9 | Write-Host No svc.conf found in $d 10 | continue 11 | } 12 | 13 | Write-Host 14 | Write-Host svc install "$d\" 15 | svc install "$d\" 16 | } -------------------------------------------------------------------------------- /bin/log4net.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandolia/easy-service/956916d12431d31c70715aa35d0fbb38b142bba7/bin/log4net.dll -------------------------------------------------------------------------------- /bin/register-this-path.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell -c [Environment]::SetEnvironmentVariable('Path',[Environment]::GetEnvironmentVariable('Path','Machine')+';%~dp0','Machine') 3 | echo register "%~dp0" to system path 4 | pause -------------------------------------------------------------------------------- /bin/register-this-path.win10.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell -c "Start-Process .\register-this-path.bat -Verb RunAs" -------------------------------------------------------------------------------- /bin/svc.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandolia/easy-service/956916d12431d31c70715aa35d0fbb38b142bba7/bin/svc.exe -------------------------------------------------------------------------------- /readme.eng.md: -------------------------------------------------------------------------------- 1 | [中文](https://github.com/pandolia/easy-service) | English 2 | 3 | ### Introduction 4 | 5 | If your Windows program needs to run permanently in the background, and you want it to be started automatically after system booting, then you need to register it as a system service. 6 | 7 | Unfortunately, making a program that can be registered as a system service is not an easy task. First, the propgram must be an executable binary, so you can't write it with script language like Python or virtual machine language like Java. Second, you must write it in accordance with the format of Windows Service Program, which is complecated. Refer to [MS official document](https://code.msdn.microsoft.com/windowsapps/CppWindowsService-cacf4948) to see how to make a Windows Service Program. 8 | 9 | [EasyService](https://github.com/pandolia/easy-service) is a small tool which can register normal program as a system service. It's only 37KB. You can write your program in normal way with whatever language you like, and register it as a system servie, then it will be started automatically after system booting, runs permanently in the background, and keeps runnning even after you logout the system. 10 | 11 | If you need to deploy website server, api server or other server that runs permanently in the background in Windows, EasyService will be a very usefull tool. 12 | 13 | ### Setup 14 | 15 | Download [the source and binary of EasyService](https://github.com/pandolia/easy-service/archive/master.zip), extract it. Then right click ***bin/register-this-path.bat*** , run with Administrator to add the path of ***bin*** directory to system path. Or add it manual. 16 | 17 | Reopen My Computer, open a terminal somewhere, run command *** svc -v *** to check whether the installation is successful. 18 | 19 | ### Usage 20 | 21 | (1) Write and test your program. EasyService has only one mandatory requirement and a recommendation to your program: 22 | 23 | ``` 24 | mandatory requirement: the program runs permanently 25 | 26 | recommendation: the program exits in 5 seconds when receives data "exit" in its stdin 27 | ``` 28 | 29 | Typical programs are: [index.js](https://github.com/pandolia/easy-service/blob/master/samples/nodejs-version/worker/index.js) (nodejs version), [main.py](https://github.com/pandolia/easy-service/blob/master/samples/python-version/worker/main.py) (Python version), and [SampleWorker.cs](https://github.com/pandolia/easy-service/blob/master/src/SampleWorker.cs)。 30 | 31 | (2) Open a terminal with administrator, run ***svc create hello-svc** to create a template project directory hello-svc. 32 | 33 | (3) Open **hello-svc/svc.conf** , edit configurations: 34 | 35 | ```conf 36 | # service's name, DO NOT conflict with existed services 37 | ServiceName: hello-svc 38 | 39 | # program and command line arguments 40 | Worker: sample-worker.exe 41 | 42 | # working directory where you want to run the program, make sure this diretory exists 43 | WorkingDir: worker 44 | 45 | # output of the program will be written to this directory, make sure this diretory exists 46 | OutFileDir: outfiles 47 | 48 | # output encoding of the program, leave empty if you are not sure 49 | WorkerEncoding: 50 | ``` 51 | 52 | (4) Cd to hello-svc: 53 | 54 | a. run ***svc check*** to check configurations 55 | 56 | b. run ***svc test-worker*** to test your program (the Worker) 57 | 58 | If everything are fine: 59 | 60 | c. run ***svc install*** to register and start a system service. Now your program is running in the background. 61 | 62 | d. run ***svc stop|start|restart|remove*** to stop, start, restart and remove the service. 63 | 64 | ### Register multiple services 65 | 66 | To register multiple services, just run ***svc create your-project-name*** to create multiple template project directories, edit configurations, and run **svc check|test-worker|install|...** . 67 | 68 | ### Internal Implementation 69 | 70 | Actually, EasyService registers himself (**svc.exe**) as a system service. When this service starts, he reads configurations in **svc.conf** and creates a child process to run the program (the Worker), then monitors the child process and re-creates one if it stops running. When this service stops (or its memory exceeds a specific value), he writes data "exit" to the stdin of the child process and wait for it to exit, and terminates the child process if waitting time exceeds 5 seconds. 71 | 72 | Source code of EasyService are in [src](https://github.com/pandolia/easy-service/tree/master/src) 。 -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 中文 | [English](./readme.eng.md) 2 | 3 | ### 介绍 4 | 5 | 如果你的 Windows 程序需要在后台长期运行,而且你希望它在开机后用户登录之前就自动运行、且在用户注销之后也不停止,那么你需要将程序注册为一个系统服务。 6 | 7 | 然而,在 Windows 下编写一个可注册为系统服务的程序并不是一件简单的事情。首先,程序必须是二进制的可执行程序,这就排除了脚本语言和虚拟机语言;其次,程序必须按系统服务的格式编写,过程繁琐,编写示例可见:[MS 官方文档](https://code.msdn.microsoft.com/windowsapps/CppWindowsService-cacf4948) 。 8 | 9 | [EasyService](https://github.com/pandolia/easy-service) 是一个可以将常规程序注册为系统服务的工具,体积只有 37KB 。你可以按常规的方法编写程序,然后用 EasyService 注册为一个系统服务,这样你的程序就可以在开机后用户登录之前自动运行、且在用户注销之后也不会停止。 10 | 11 | 如果你需要在 Windows 系统下部署网站、API 或其他需要长期在后台运行的服务, EasyService 将是一个很有用的工具。 12 | 13 | ### 安装 14 | 15 | 下载 [源码及程序](https://github.com/pandolia/easy-service/archive/master.zip),解压。右键单击 bin 目录下的 register-this-path.bat ,以管理员身份运行,将 bin 目录加入至系统路径中,也可以手动将此目录加入至系统路径。 16 | 17 | 重新打开 “我的电脑” ,在任意位置打开一个命令行窗口,输入 ***svc -v*** ,如果正常输出版本信息,则表明安装成功。 18 | 19 | ### 使用方法 20 | 21 | (1) 编写、测试你的程序,EasyService 对程序仅有一个强制要求和一个建议: 22 | 23 | * 强制要求: 程序应持续运行 24 | 25 | * 建议: 当程序的标准输入接收到 “exit” 或 “回车” 后在 5 秒之内退出 26 | 27 | 其中建议要求是非强制性的,程序不满足此要求也可以。 28 | 29 | 典型的程序见 [index.js](https://github.com/pandolia/easy-service/blob/master/samples/nodejs-version/worker/index.js) (nodejs 版), [main.py](https://github.com/pandolia/easy-service/blob/master/samples/python-version/worker/main.py) (python 版) 或 [SampleWorker.cs](https://github.com/pandolia/easy-service/blob/master/src/SampleWorker.cs) (C# 版),这三个程序都是每隔 1 秒打印一行信息,键入回车后退出。 30 | 31 | (2) 打开命令行窗口,输入 ***svc create hello-svc*** ,将创建一个样板工程 hello-svc 。 32 | 33 | (3) 打开 hello-svc/svc.conf 文件,修改配置: 34 | 35 | ```conf 36 | # Windows 系统服务名称、不能与系统中已有服务重名 37 | ServiceName: hello-svc 38 | 39 | # 需要运行的可执行程序及命令行参数 40 | Worker: sample-worker.exe 41 | 42 | # 程序运行的工作目录,请确保该目录已存在 43 | WorkingDir: worker 44 | 45 | # 输出目录,程序运行过程的输出将会写到这个目录下面,请确保该目录已存在 46 | # 如果不想保存 Worker 的输出,请设为 $NULL 47 | OutFileDir: outfiles 48 | 49 | # 程序输出的编码,如果不确定,请设为空 50 | WorkerEncoding: utf8 51 | ``` 52 | 53 | (4) 用管理员身份打开命令行窗口(Win10 系统下,需要在开始菜单中搜索 cmd 然后右键以管理员身份运行), cd 到 hello-svc 目录: 54 | 55 | a. 运行 ***svc check*** 命令检查配置是否合法 56 | 57 | b. 运行 ***svc test-worker*** 命令测试 Worker 程序是否能正常运行 58 | 59 | 若测试无误: 60 | 61 | c. 运行 ***svc install*** 命令安装并启动系统服务,此时程序就已经开始在后台运行了 62 | 63 | d. 运行 ***svc stop|start|restart|remove*** 停止、启动、重启或删除本系统服务 64 | 65 | ### 注册多个服务 66 | 67 | 如果需要注册多个服务,可以用 ***svc create*** 创建多个目录,修改 svc.conf 中的服务名和程序名等内容,再在这些目录下打开命令行窗口执行 svc check|test-worker|install 等命令就可以了。需要注意的是: 68 | 69 | * (1) 不同目录下的服务名不能相同,也不能和系统已有的服务同名 70 | 71 | * (2) 配置文件中的 Worker/WorkingDir/OutFileDir 都是相对于该配置文件的路径 72 | 73 | * (3) 注册服务之前,WorkingDir/OutFileDir 所指定的目录必须先创建好 74 | 75 | ### EasyService 服务操作命令 76 | 77 | 以下命令可以操作 EasyService 服务(用 svc 命令注册的服务),这些命令可在任意位置运行,不需要 cd 到 svc.conf 所在的目录: 78 | 79 | * svc list|ls: 列出所有服务 80 | 81 | * svc start|stop|remove all: 启动、停止或删除所有服务 82 | 83 | * svc check|status|test-worker|install|start|stop|restart|remove $project-directory: 操作 $project-directory 目录下 svc.conf 指定的服务($project-directory 中必须含有字符 \\ 或 /) 84 | 85 | * svc start|stop|restart|remove $service-index: 操作第 $service-index 个服务($service-index 为数字,运行 svc ls 可查看所有服务的序号) 86 | 87 | * svc start|stop|restart|remove $service-name: 操作名称为 $service-name 的服务($service-name 不全为数字、不包含 \\ 或 / ,且不为 all ) 88 | 89 | 以 start 命令为例: 90 | 91 | * svc start all: 启动所有服务 92 | 93 | * svc start aa\: 启动 aa\svc.conf 文件指定的服务 94 | 95 | * svc start aa: 启动名称为 aa 的服务 96 | 97 | * svc start 2: 启动第 2 个服务 98 | 99 | 注意: check|status|test-worker|install 命令只支持 $project-directory 模式, restart 命令不支持 all 模式。 100 | 101 | ### 注意事项 102 | 103 | 为保证服务的一致性,要求: 104 | 105 | * (1) 运行 ***svc install*** 安装服务之后,在运行 ***svc remove*** 删除服务之前,不应对 svc.conf 文件进行修改,删除,也不应移动或重命名目录 106 | 107 | * (2) 不应在服务管理控制台中对采用 EasyService 安装的服务进行修改或操作,也不应采用除 svc 命令以外的其他方式进行修改或操作 108 | 109 | ### 配置项 110 | 111 | ```conf 112 | # 服务名称,不能与系统中已有服务重名 113 | ServiceName: easy-service 114 | 115 | # 服务显示名称,不能与系统中已有服务的显示名称相同 116 | DisplayName: easy-service 117 | 118 | # 服务描述 119 | Description: An example of EasyService 120 | 121 | # 本服务依赖的服务名列表,用逗号分开,例如: Appinfo,AppMgmt 122 | Dependencies: 123 | 124 | # 需要运行的可执行程序及命令行参数 125 | Worker: sample-worker.exe 126 | 127 | # 运行程序的环境变量 128 | Environments: TEST-ENV1=A1,TEST-ENV2=A2,TEST-ENV3=A3 129 | Environments: TEST-ENV4=A4,TEST-ENV5=A5,TEST-ENV6=A6 130 | 131 | # 程序运行的工作目录,请确保该目录已存在 132 | WorkingDir: worker 133 | 134 | # 输出目录,程序运行过程的输出将会写到这个目录下面,请确保该目录已存在 135 | # 如果不想保存 Worker 的输出,请设为 $NULL 136 | OutFileDir: outfiles 137 | 138 | # 停止服务时,等待程序主动退出的最大时间 139 | WaitSecondsForWorkerToExit: 5 140 | 141 | # 注意: MaxLogFilesNum配置已废弃(自v1.0.11) 142 | # 日志文件的最大个数,设为空则不限制,否则需要设置为大于等于 2 的整数 143 | # svc.exe 每隔两个小时检查一次日志文件个数,如果个数大于这个值,就删除最老的几个文件 144 | MaxLogFilesNum: 145 | 146 | # 程序的内存使用限制值 147 | WorkerMemoryLimit: 148 | 149 | # 程序输出的编码,如果不确定,请设为空 150 | WorkerEncoding: utf8 151 | 152 | # 运行本服务的用户,一般情况下用 LOCAL-SYSTEM ,下面三个项都设为空 153 | # 如果使用普通用户,下面三个项, 域、用户名、密码都要设置,而且要在服务管理面板里面授权此用户运行服务的权限 154 | # 域如果不确定,可以设置为 “." 155 | Domain: 156 | User: 157 | Password: 158 | ``` 159 | 160 | ### 内部实现 161 | 162 | EasyService 实质是将自己(svc.exe)注册为一个系统服务,此服务启动时,会读取 svc.conf 中的配置,创建一个子进程运行 Worker 中指定的程序及命令行参数,之后,监视该子进程,如果发现 ***子进程停止运行(或内存使用量超过指定值)*** ,会重新启动一个子进程。而当此服务停止时,按以下原则终止子进程: 163 | 164 | * 当 WaitSecondsForWorkerToExit 为 0 时,直接终止子进程 165 | 166 | * 当 WaitSecondsForWorkerToExit 不为 0 时,向子进程的标准输入中写入数据 “exit” ,并等待子进程退出,若等待时间超过 WaitSecondsForWorkerToExit 秒,则终止子进程。 167 | 168 | EasyService 源码见 [src](https://github.com/pandolia/easy-service/tree/master/src) 。 169 | 170 | ### 与 NSSM 的对比 171 | 172 | Windows 下部署服务的同类型的工具还有 NSSM ,与 EasyService 相比, NSSM 主要优点有: 173 | 174 | * 提供了图形化安装、管理服务的界面 175 | 176 | NSSM 主要缺点是界面和文档都是英文的,对新手也不见得更友好,另外在远程通过命令行编辑和管理服务稍微麻烦一些,需要记住它的命令的参数。 177 | 178 | 总体而言, EasyService 已实现了大部分服务程序需要的功能,主要优点有: 179 | 180 | * 在命令行模式下编辑、管理和查看服务更方便 181 | 182 | * 日志自动按日期输出到不同文件 183 | 184 | * 停止服务时,可先向工作进程的标准输入写入 "exit" ,并等待工作进程自己退出,这个 “通知退出” 的机制对于需要进行清理工作的程序来说是非常关键的 185 | 186 | ### 典型用例 187 | 188 | Appin 网站介绍了用 EasyService 部署 frp 内网穿透服务的方法,请看 [这里](https://www.appinn.com/easyservice-for-windows/) 。 189 | 190 | ### 意见反馈 191 | 192 | 若对 EasyService 有任何意见或建议,可以提 ISSUE ,也可以加作者微信 Kotlinfinity 。 193 | -------------------------------------------------------------------------------- /samples/csharp-version/outfiles/lastline.log: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /samples/csharp-version/svc.conf: -------------------------------------------------------------------------------- 1 | # EasyService's configurations file 2 | 3 | # Commands: 4 | # svc version|-v|--version 5 | # svc create $project-name 6 | # svc check|status [$project-directory] 7 | # svc test-worker [$project-directory] 8 | # svc install [$project-directory] 9 | # svc stop|start|remove [$project-directory|$service-name|$service-index|all] 10 | # svc restart|log [$project-directory|$service-name|$service-index] 11 | # svc list|ls 12 | 13 | # Note: $project-directory must contain '\' or '/' 14 | 15 | # Documentation: readme.md, readme.eng.md, https://github.com/pandolia/easy-service 16 | 17 | # After `svc install`, DO NOT EDIT OR DELETE THIS FILE before `svc remove` 18 | 19 | # DO NOT EDIT OR OPERATE THIS SERVICE in "Service Manage Console (services.msc)" 20 | 21 | ServiceName: easy-service 22 | 23 | DisplayName: easy-service 24 | 25 | Description: An example of EasyService 26 | 27 | # i.e Appinfo,AppMgmt 28 | Dependencies: 29 | 30 | Worker: sample-worker.exe 31 | 32 | # Worker's enrinonment variables 33 | Environments: TEST-ENV1=A1,TEST-ENV2=A2,TEST-ENV3=A3 34 | Environments: TEST-ENV4=A4,TEST-ENV5=A5,TEST-ENV6=A6 35 | 36 | WorkingDir: worker 37 | 38 | OutFileDir: outfiles 39 | 40 | WaitSecondsForWorkerToExit: 5 41 | 42 | # leave empty or set to a number greater than or equals 2 43 | # if the count of log files is greater than this number, old log files will be deleted 44 | MaxLogFilesNum: 45 | 46 | # worker's memory usage limitation 47 | # the worker will be killed and recreated when its memory usage exceeds this value 48 | # leave empty to turn off memory usage limitation 49 | WorkerMemoryLimit: 50 | 51 | # encoding of the worker, i.e utf8|cp936|... , leave empty to use system's default encoding 52 | WorkerEncoding: 53 | 54 | # user who you want to run this service, leave empty to use LOCAL-SYSTEM 55 | # if using normal user, you must grant him the permission of running services at "Service Manage Console" 56 | Domain: 57 | User: 58 | Password: -------------------------------------------------------------------------------- /samples/csharp-version/worker/sample-worker.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandolia/easy-service/956916d12431d31c70715aa35d0fbb38b142bba7/samples/csharp-version/worker/sample-worker.exe -------------------------------------------------------------------------------- /samples/nodejs-version/outfiles/lastline.log: -------------------------------------------------------------------------------- 1 | [2020-3-8 16:37:30] Stopped SampleWorker(node.js version) 2 | -------------------------------------------------------------------------------- /samples/nodejs-version/svc.conf: -------------------------------------------------------------------------------- 1 | # EasyService's configurations file 2 | 3 | # Commands: 4 | # svc version|-v|--version 5 | # svc create $project-name 6 | # svc check|status [$project-directory] 7 | # svc test-worker [$project-directory] 8 | # svc install [$project-directory] 9 | # svc stop|start|remove [$project-directory|$service-name|$service-index|all] 10 | # svc restart|log [$project-directory|$service-name|$service-index] 11 | # svc list|ls 12 | 13 | # Note: $project-directory must contain '\' or '/' 14 | 15 | # Documentation: readme.md, readme.eng.md, https://github.com/pandolia/easy-service 16 | 17 | # After `svc install`, DO NOT EDIT OR DELETE THIS FILE before `svc remove` 18 | 19 | # DO NOT EDIT OR OPERATE THIS SERVICE in "Service Manage Console (services.msc)" 20 | 21 | ServiceName: easy-service 22 | 23 | DisplayName: easy-service 24 | 25 | Description: An example of EasyService 26 | 27 | # i.e Appinfo,AppMgmt 28 | Dependencies: 29 | 30 | Worker: node index.js 31 | 32 | # Worker's enrinonment variables 33 | Environments: TEST-ENV1=A1,TEST-ENV2=A2,TEST-ENV3=A3 34 | Environments: TEST-ENV4=A4,TEST-ENV5=A5,TEST-ENV6=A6 35 | 36 | WorkingDir: worker 37 | 38 | OutFileDir: outfiles 39 | 40 | WaitSecondsForWorkerToExit: 5 41 | 42 | # leave empty or set to a number greater than or equals 2 43 | # if the count of log files is greater than this number, old log files will be deleted 44 | MaxLogFilesNum: 45 | 46 | # worker's memory usage limitation 47 | # the worker will be killed and recreated when its memory usage exceeds this value 48 | # leave empty to turn off memory usage limitation 49 | WorkerMemoryLimit: 50 | 51 | # encoding of the worker, i.e utf8|cp936|... , leave empty to use system's default encoding 52 | WorkerEncoding: utf8 53 | 54 | # user who you want to run this service, leave empty to use LOCAL-SYSTEM 55 | # if using normal user, you must grant him the permission of running services at "Service Manage Console" 56 | Domain: 57 | User: 58 | Password: -------------------------------------------------------------------------------- /samples/nodejs-version/worker/index.js: -------------------------------------------------------------------------------- 1 | function log(s) { 2 | var t = new Date(); 3 | console.log(`[${t.toLocaleString()}] ${s}`); 4 | } 5 | 6 | log('Started SampleWorker(node.js version), press enter to exit') 7 | 8 | var intv = setInterval(function () { log('Running'); }, 1000); 9 | 10 | process.stdin.on('data', function (data) { 11 | log(`Received message "${data.toString().trim()}" from the Monitor`); 12 | clearInterval(intv); 13 | log('Stopped SampleWorker(node.js version)'); 14 | process.exit(); 15 | }); -------------------------------------------------------------------------------- /samples/python-version/outfiles/lastline.log: -------------------------------------------------------------------------------- 1 | [2020-3-8 16:37:30] Stopped SampleWorker(node.js version) 2 | -------------------------------------------------------------------------------- /samples/python-version/svc.conf: -------------------------------------------------------------------------------- 1 | # EasyService's configurations file 2 | 3 | # Commands: 4 | # svc version|-v|--version 5 | # svc create $project-name 6 | # svc check|status [$project-directory] 7 | # svc test-worker [$project-directory] 8 | # svc install [$project-directory] 9 | # svc stop|start|remove [$project-directory|$service-name|$service-index|all] 10 | # svc restart|log [$project-directory|$service-name|$service-index] 11 | # svc list|ls 12 | 13 | # Note: $project-directory must contain '\' or '/' 14 | 15 | # Documentation: readme.md, readme.eng.md, https://github.com/pandolia/easy-service 16 | 17 | # After `svc install`, DO NOT EDIT OR DELETE THIS FILE before `svc remove` 18 | 19 | # DO NOT EDIT OR OPERATE THIS SERVICE in "Service Manage Console (services.msc)" 20 | 21 | ServiceName: easy-service 22 | 23 | DisplayName: easy-service 24 | 25 | Description: An example of EasyService 26 | 27 | # i.e Appinfo,AppMgmt 28 | Dependencies: 29 | 30 | # some time you must provide full path of python.exe 31 | Worker: C:\Python27\python.exe main.py 32 | 33 | # Worker's enrinonment variables 34 | Environments: TEST-ENV1=A1,TEST-ENV2=A2,TEST-ENV3=A3 35 | Environments: TEST-ENV4=A4,TEST-ENV5=A5,TEST-ENV6=A6 36 | 37 | WorkingDir: worker 38 | 39 | OutFileDir: outfiles 40 | 41 | WaitSecondsForWorkerToExit: 5 42 | 43 | # leave empty or set to a number greater than or equals 2 44 | # if the count of log files is greater than this number, old log files will be deleted 45 | MaxLogFilesNum: 46 | 47 | # worker's memory usage limitation 48 | # the worker will be killed and recreated when its memory usage exceeds this value 49 | # leave empty to turn off memory usage limitation 50 | WorkerMemoryLimit: 51 | 52 | # encoding of the worker, i.e utf8|cp936|... , leave empty to use system's default encoding 53 | WorkerEncoding: utf8 54 | 55 | # user who you want to run this service, leave empty to use LOCAL-SYSTEM 56 | # if using normal user, you must grant him the permission of running services at "Service Manage Console" 57 | Domain: 58 | User: 59 | Password: -------------------------------------------------------------------------------- /samples/python-version/worker/main.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import datetime 4 | import sys 5 | 6 | def log(s): 7 | t = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 8 | sys.stdout.write('[%s] %s\n' % (t, s)) 9 | sys.stdout.flush() 10 | 11 | def loop(): 12 | log('Started SampleWorker(python version), press enter to exit') 13 | while running: 14 | log('Running') 15 | time.sleep(1) 16 | log('Stopped SampleWorker(python version)') 17 | 18 | running = True 19 | th = threading.Thread(target=loop) 20 | th.start() 21 | 22 | try: 23 | msg = raw_input() 24 | except: 25 | msg = input() 26 | 27 | log('Received message "%s" from the Monitor' % msg) 28 | 29 | running = False 30 | th.join() -------------------------------------------------------------------------------- /src/Conf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Collections.Generic; 5 | 6 | public class Conf 7 | { 8 | public string ServiceName { get; private set; } = null; 9 | 10 | public string DisplayName { get; private set; } = null; 11 | 12 | public string Description { get; private set; } = null; 13 | 14 | public string Dependencies { get; private set; } = null; 15 | 16 | public string Worker { get; private set; } = null; 17 | 18 | public string WorkingDir { get; private set; } = null; 19 | 20 | public string OutFileDir { get; private set; } = null; 21 | 22 | public int WaitSecondsForWorkerToExit { get; private set; } = 0; 23 | 24 | public int MaxLogFilesNum { get; private set; } = 0; 25 | 26 | public string WorkerEncoding { get; private set; } = null; 27 | 28 | public int WorkerMemoryLimit { get; private set; } = -1; 29 | 30 | public string Domain { get; private set; } = null; 31 | 32 | public string User { get; private set; } = null; 33 | 34 | public string Password { get; private set; } = null; 35 | 36 | public string WorkerFileName { get; private set; } = null; 37 | 38 | public string WorkerArguments { get; private set; } = null; 39 | 40 | public Encoding WorkerEncodingObj { get; private set; } = null; 41 | 42 | public readonly Dictionary Environments = new Dictionary(); 43 | 44 | public Conf(Action abort = null) 45 | { 46 | var setterDict = new Dictionary> 47 | { 48 | { "setServiceName", SetServiceName }, 49 | { "setDisplayName", SetDisplayName }, 50 | { "setDescription", SetDescription }, 51 | { "setDependencies", SetDependencies }, 52 | { "setWorker", SetWorker }, 53 | { "setEnvironments", SetEnvironments }, 54 | { "setWorkingDir", SetWorkingDir }, 55 | { "setOutFileDir", SetOutFileDir }, 56 | { "setWaitSecondsForWorkerToExit", SetWaitSecondsForWorkerToExit }, 57 | { "setMaxLogFilesNum", SetMaxLogFilesNum }, 58 | { "setWorkerEncoding", SetWorkerEncoding }, 59 | { "setWorkerMemoryLimit", SetWorkerMemoryLimit }, 60 | { "setDomain", SetDomain }, 61 | { "setUser", SetUser }, 62 | { "setPassword", SetPassword } 63 | }; 64 | 65 | var err = Libs.ReadConfig(Consts.CONF_FILE, setterDict); 66 | if (err != null) 67 | { 68 | abort = abort ?? Libs.Abort; 69 | abort($"Configuration Error: {err}"); 70 | } 71 | 72 | ResolvePaths(); 73 | 74 | if (DisplayName == null || DisplayName.Length == 0) 75 | { 76 | DisplayName = ServiceName; 77 | } 78 | } 79 | 80 | private void ResolvePaths() 81 | { 82 | WorkingDir = Path.GetFullPath(WorkingDir); 83 | OutFileDir = OutFileDir != null ? Path.GetFullPath(OutFileDir) : null; 84 | 85 | var suffixes = new string[] { "", ".exe", ".bat" }; 86 | foreach (var suffix in suffixes) 87 | { 88 | if (suffix.Length > 0 && WorkerFileName.EndsWith(suffix)) 89 | { 90 | continue; 91 | } 92 | 93 | var realName = Path.Combine(WorkingDir, WorkerFileName + suffix); 94 | if (File.Exists(realName)) 95 | { 96 | WorkerFileName = realName; 97 | break; 98 | } 99 | } 100 | } 101 | 102 | public void ShowConfig() 103 | { 104 | var maxLogFilesNumString = (MaxLogFilesNum == 0) ? "" : $"{MaxLogFilesNum}"; 105 | 106 | Console.WriteLine($"ServiceName: {ServiceName}"); 107 | Console.WriteLine($"DisplayName: {DisplayName}"); 108 | Console.WriteLine($"Description: {Description}"); 109 | Console.WriteLine($"Dependencies: {Dependencies}"); 110 | Console.WriteLine($"Worker: {Worker}"); 111 | Console.WriteLine($"Worker's fileName: {WorkerFileName}"); 112 | Console.WriteLine($"Worker's arguments: {WorkerArguments}"); 113 | Console.WriteLine($"Worker's enrinonments: {Environments.ToPrettyString()}"); 114 | Console.WriteLine($"WorkingDir: {WorkingDir}"); 115 | Console.WriteLine($"OutFileDir: {OutFileDir}"); 116 | Console.WriteLine($"WaitSecondsForWorkerToExit: {WaitSecondsForWorkerToExit}"); 117 | Console.WriteLine($"MaxLogFilesNum: {maxLogFilesNumString} (WARN: This property is deprecated since v1.0.11)"); 118 | Console.WriteLine($"WorkerEncoding: {WorkerEncoding}"); 119 | Console.WriteLine($"WorkerMemoryLimit: {WorkerMemoryLimit}"); 120 | Console.WriteLine($"Domain: {Domain}"); 121 | Console.WriteLine($"User: {User}"); 122 | Console.WriteLine($"Password: {Password}"); 123 | } 124 | 125 | public static string CheckServiceName(string name) 126 | { 127 | if (name.Length == 0) 128 | { 129 | return "empty"; 130 | } 131 | 132 | if (name.Contains('"')) 133 | { 134 | return "contains '\"'"; 135 | } 136 | 137 | if (name.Contains('\\') || name.Contains('/')) 138 | { 139 | return "contains '\\' or '/'"; 140 | } 141 | 142 | if (name.Contains('[') || name.Contains(']')) 143 | { 144 | return "contains '[' or ']'"; 145 | } 146 | 147 | if (name.IsDigit()) 148 | { 149 | return "is digit"; 150 | } 151 | 152 | return null; 153 | } 154 | 155 | private string SetServiceName(string value) 156 | { 157 | ServiceName = value; 158 | return CheckServiceName(value); 159 | } 160 | 161 | private string SetDisplayName(string value) 162 | { 163 | DisplayName = value; 164 | 165 | if (value.Contains("\"")) 166 | { 167 | return "contains '\"'"; 168 | } 169 | 170 | return null; 171 | } 172 | 173 | private string SetDescription(string value) 174 | { 175 | Description = value; 176 | 177 | if (value.Contains("\"")) 178 | { 179 | return "contains '\"'"; 180 | } 181 | 182 | return null; 183 | } 184 | 185 | private string SetDependencies(string value) 186 | { 187 | Dependencies = value.Replace(',', '/'); 188 | 189 | if (value.Contains("\"")) 190 | { 191 | return "contains '\"'"; 192 | } 193 | 194 | return null; 195 | } 196 | 197 | private string SetWorker(string value) 198 | { 199 | Worker = value; 200 | 201 | if (Worker.Length == 0) 202 | { 203 | return "emtpy"; 204 | } 205 | 206 | if (Worker[0] == '"') 207 | { 208 | var i = Worker.IndexOf('"', 1); 209 | if (i == -1) 210 | { 211 | return "bad format"; 212 | } 213 | 214 | WorkerFileName = Worker.Substring(1, i - 1).Trim(); 215 | if (WorkerFileName.Length == 0) 216 | { 217 | return "bad format"; 218 | } 219 | 220 | WorkerArguments = Worker.Substring(i + 1).TrimStart(); 221 | return null; 222 | } 223 | 224 | var ii = Worker.IndexOf(' '); 225 | if (ii == -1) 226 | { 227 | WorkerFileName = Worker; 228 | WorkerArguments = ""; 229 | return null; 230 | } 231 | 232 | WorkerFileName = Worker.Substring(0, ii); 233 | WorkerArguments = Worker.Substring(ii + 1).TrimStart(); 234 | return null; 235 | } 236 | 237 | private string SetEnvironments(string value) 238 | { 239 | foreach (var seg0 in value.Split(',')) 240 | { 241 | var seg = seg0.Trim(); 242 | var n = seg.Length; 243 | 244 | if (n == 0) 245 | { 246 | continue; 247 | } 248 | 249 | var i = seg.IndexOf('='); 250 | 251 | if (i == -1 || i == 0) 252 | { 253 | return "bad format"; 254 | } 255 | 256 | var key = seg.Substring(0, i).TrimEnd(); 257 | var val = (i == n - 1) ? "" : seg.Substring(i + 1, n - i - 1).TrimStart(); 258 | 259 | Environments[key] = val; 260 | } 261 | 262 | return null; 263 | } 264 | 265 | private string SetWorkingDir(string value) 266 | { 267 | WorkingDir = value; 268 | if (!Directory.Exists(WorkingDir)) 269 | { 270 | return "directory not exists"; 271 | } 272 | 273 | return null; 274 | } 275 | 276 | private string SetOutFileDir(string value) 277 | { 278 | if (value == "$NULL") 279 | { 280 | OutFileDir = null; 281 | return null; 282 | } 283 | 284 | if (!Directory.Exists(value)) 285 | { 286 | return "directory not exists"; 287 | } 288 | 289 | if (!value.EndsWith("\\")) 290 | { 291 | value += "\\"; 292 | } 293 | 294 | OutFileDir = value; 295 | 296 | return null; 297 | } 298 | 299 | private string SetWaitSecondsForWorkerToExit(string value) 300 | { 301 | if (value == "") 302 | { 303 | WaitSecondsForWorkerToExit = 0; 304 | return null; 305 | } 306 | 307 | if (!int.TryParse(value, out int val) || val < 0 || val > 300) 308 | { 309 | return "should be a number between 0 ~ 300"; 310 | } 311 | 312 | WaitSecondsForWorkerToExit = val; 313 | return null; 314 | } 315 | 316 | private string SetMaxLogFilesNum(string value) 317 | { 318 | if (value == "") 319 | { 320 | MaxLogFilesNum = 0; 321 | return null; 322 | } 323 | 324 | if (!int.TryParse(value, out int val) || val < 2) 325 | { 326 | return "should be a number greater than or equals 2"; 327 | } 328 | 329 | MaxLogFilesNum = val; 330 | return null; 331 | } 332 | 333 | private string SetWorkerEncoding(string value) 334 | { 335 | WorkerEncoding = value; 336 | 337 | value = value.ToLower(); 338 | 339 | if (value == "" || value == "null") 340 | { 341 | WorkerEncoding = "null"; 342 | WorkerEncodingObj = null; 343 | return null; 344 | } 345 | 346 | if (value.StartsWith("utf") && !value.StartsWith("utf-")) 347 | { 348 | value = "utf-" + value.Substring(3); 349 | } 350 | 351 | try 352 | { 353 | WorkerEncodingObj = Encoding.GetEncoding(value); 354 | } 355 | catch (Exception e) 356 | { 357 | return e.Message; 358 | } 359 | 360 | return null; 361 | } 362 | 363 | private string SetWorkerMemoryLimit(string value) 364 | { 365 | if (value == "") 366 | { 367 | WorkerMemoryLimit = -1; 368 | return null; 369 | } 370 | 371 | if (!int.TryParse(value, out int val) || val < 1 || val > 5000000) 372 | { 373 | return "should be a number between 1 ~ 5000000"; 374 | } 375 | 376 | WorkerMemoryLimit = val; 377 | return null; 378 | } 379 | 380 | private string SetDomain(string value) 381 | { 382 | Domain = value; 383 | 384 | if (value.Contains("\"")) 385 | { 386 | return "contains '\"'"; 387 | } 388 | 389 | return null; 390 | } 391 | 392 | private string SetUser(string value) 393 | { 394 | User = value; 395 | 396 | if (value.Contains("\"")) 397 | { 398 | return "contains '\"'"; 399 | } 400 | 401 | return null; 402 | } 403 | 404 | private string SetPassword(string value) 405 | { 406 | Password = value; 407 | 408 | if (value.Contains("\"")) 409 | { 410 | return "contains '\"'"; 411 | } 412 | 413 | return null; 414 | } 415 | } -------------------------------------------------------------------------------- /src/Consts.cs: -------------------------------------------------------------------------------- 1 | static class Consts 2 | { 3 | public const string CONF_FILE = "svc.conf"; 4 | 5 | public const string LOG_FILE = "svc.log"; 6 | 7 | public const string NECESSARY_DEPENDENCY = "RpcLocator"; 8 | 9 | public const int RESTART_WAIT_SECONDS = 5; 10 | 11 | public const int MAX_SERVICE_START_STOP_SECONDS = 12; 12 | 13 | public const int MONITOR_INTERVAL_MINUTES = 2; 14 | } -------------------------------------------------------------------------------- /src/EasyService.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.352 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Main", "Main.csproj", "{41E81318-4181-4912-ACFA-D959AAE3E855}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWorker", "SampleWorker.csproj", "{D4F448E9-9122-4A63-BB3E-1996CD01D7FE}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {41E81318-4181-4912-ACFA-D959AAE3E855}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {41E81318-4181-4912-ACFA-D959AAE3E855}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {D4F448E9-9122-4A63-BB3E-1996CD01D7FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {D4F448E9-9122-4A63-BB3E-1996CD01D7FE}.Release|Any CPU.Build.0 = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(SolutionProperties) = preSolution 21 | HideSolutionNode = FALSE 22 | EndGlobalSection 23 | GlobalSection(ExtensibilityGlobals) = postSolution 24 | SolutionGuid = {0A16984A-AA11-4E08-8292-77327904E16B} 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /src/Libs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Management; 8 | using System.Text.RegularExpressions; 9 | 10 | public delegate string FieldSetter(string value); 11 | 12 | public static class Libs 13 | { 14 | public static readonly string BinDir = AppDomain.CurrentDomain.BaseDirectory; 15 | 16 | public static readonly string BinPath = Assembly.GetExecutingAssembly().Location; 17 | 18 | public static string GetCwd() => Path.GetFullPath(".") + "\\"; 19 | 20 | public static void SetCwd(string d) 21 | { 22 | Directory.SetCurrentDirectory(d); 23 | } 24 | 25 | public static List GetFiles(string directory, string pattern) 26 | { 27 | var result = new List(); 28 | var regex = new Regex(pattern); 29 | var d = new DirectoryInfo(directory); 30 | var files = d.GetFiles(); 31 | foreach (var f in files) 32 | { 33 | if (regex.IsMatch(f.Name)) 34 | { 35 | result.Add(f); 36 | } 37 | } 38 | 39 | result.Sort((x, y) => x.Name.CompareTo(y.Name)); 40 | 41 | return result; 42 | } 43 | 44 | public static List Front(this List list, int n) 45 | { 46 | var result = new List(); 47 | for (int i = 0; i < n; i++) 48 | { 49 | result.Add(list[i]); 50 | } 51 | return result; 52 | } 53 | 54 | public static string AddUniq(this string s, string p, char c) 55 | { 56 | if (s == p || s == "") 57 | { 58 | return p; 59 | } 60 | 61 | if (p == "") 62 | { 63 | return s; 64 | } 65 | 66 | if (s.StartsWith(p + c) || s.EndsWith(c + p) || s.Contains(c + p + c)) 67 | { 68 | return s; 69 | } 70 | 71 | return s + c + p; 72 | 73 | } 74 | 75 | public static void Abort(string msg) 76 | { 77 | Console.WriteLine(msg); 78 | Environment.Exit(1); 79 | } 80 | 81 | public static void Exit(string msg) 82 | { 83 | Console.WriteLine(msg); 84 | Environment.Exit(0); 85 | } 86 | 87 | public static void Dump(string s) 88 | { 89 | var pid = Process.GetCurrentProcess().Id; 90 | var filename = $"{BinDir}\\{DateTime.Now:yyyy-MM-dd-HH-mm-ss-ffff}-{pid}.error.txt"; 91 | try 92 | { 93 | WriteLineToFile(filename, s); 94 | } 95 | catch (Exception ex) 96 | { 97 | Console.WriteLine(ex); 98 | } 99 | } 100 | 101 | public static void Timeout(Action f, int seconds, string startMsg, string timeoutMsg, string okMsg) 102 | { 103 | Console.Write($"{startMsg}..."); 104 | Console.Out.Flush(); 105 | 106 | var flag = new bool[1] { false }; 107 | NewThread(() => Waiting(flag, (seconds + 1) * 1000, timeoutMsg)); 108 | f(); 109 | flag[0] = true; 110 | 111 | Console.Write($" [√]"); 112 | Console.Out.Flush(); 113 | Thread.Sleep(500); 114 | Console.WriteLine($"\r{okMsg}{"".PadRight(startMsg.Length + 10 + seconds)}"); 115 | } 116 | 117 | private static void Waiting(bool[] flag, int milliseconds, string timeoutMsg) 118 | { 119 | Thread.Sleep(2000); 120 | if (flag[0]) 121 | { 122 | return; 123 | } 124 | 125 | for (int t = 2000; t < milliseconds; t += 1500) 126 | { 127 | Thread.Sleep(1500); 128 | if (flag[0]) 129 | { 130 | return; 131 | } 132 | 133 | Console.Write("."); 134 | Console.Out.Flush(); 135 | } 136 | 137 | if (flag[0]) 138 | { 139 | return; 140 | } 141 | 142 | Console.WriteLine($"\r\n{timeoutMsg}"); 143 | Environment.Exit(1); 144 | } 145 | 146 | public static void WriteLineToFile(string file, string s, bool append = false) 147 | { 148 | using (var sw = new StreamWriter(file, append)) 149 | { 150 | sw.WriteLine(s); 151 | sw.Flush(); 152 | } 153 | } 154 | 155 | public static int Exec(string file, string args = null, string dir = null) 156 | { 157 | try 158 | { 159 | using (var p = new Process()) 160 | { 161 | p.StartInfo.FileName = file; 162 | p.StartInfo.Arguments = args; 163 | p.StartInfo.WorkingDirectory = dir; 164 | p.StartInfo.UseShellExecute = false; 165 | p.StartInfo.RedirectStandardOutput = true; 166 | p.Start(); 167 | p.WaitForExit(); 168 | if (p.ExitCode != 0) 169 | { 170 | Console.WriteLine(p.StandardOutput.ReadToEnd()); 171 | } 172 | return p.ExitCode; 173 | } 174 | } 175 | catch (Exception e) 176 | { 177 | Console.WriteLine(e.Message); 178 | return 1; 179 | } 180 | } 181 | 182 | public static void ReplaceStringInFile(string file, string oldStr, string newStr) 183 | { 184 | string content; 185 | 186 | using (var sr = new StreamReader(file)) 187 | { 188 | content = sr.ReadToEnd(); 189 | } 190 | 191 | content = content.Replace(oldStr, newStr); 192 | 193 | using (var sw = new StreamWriter(file)) 194 | { 195 | sw.Write(content); 196 | } 197 | } 198 | 199 | public static string ReadConfig(string file, Dictionary> setterDict) 200 | { 201 | string text; 202 | try 203 | { 204 | using (var sr = new StreamReader(file)) 205 | { 206 | text = sr.ReadToEnd(); 207 | } 208 | } 209 | catch (Exception e) 210 | { 211 | return e.Message; 212 | } 213 | 214 | var errs = new List(); 215 | 216 | var items = new Dictionary(); 217 | 218 | foreach (var line in text.Split('\n')) 219 | { 220 | var s = line.Replace('\t', ' ').Trim(); 221 | 222 | if (s.Length == 0 || s[0] == '#') 223 | { 224 | continue; 225 | } 226 | 227 | var i = s.IndexOf(':'); 228 | var key = (i == -1) ? s : s.Substring(0, i).TrimEnd(); 229 | var sKey = "set" + key; 230 | if (!setterDict.ContainsKey(sKey)) 231 | { 232 | errs.Add($"Invalid configuration key `{key}`"); 233 | continue; 234 | } 235 | 236 | var value = (i == -1) ? "" : s.Substring(i + 1).TrimStart(); 237 | var valueErr = setterDict[sKey](value); 238 | if (valueErr != null) 239 | { 240 | errs.Add($"Bad {key} `{value}`: {valueErr}"); 241 | } 242 | 243 | items[key] = value; 244 | } 245 | 246 | foreach (var sKey in setterDict.Keys) 247 | { 248 | var key = sKey.Substring(3); 249 | if (items.ContainsKey(key)) 250 | { 251 | continue; 252 | } 253 | 254 | var valueErr = setterDict[sKey](""); 255 | if (valueErr != null) 256 | { 257 | errs.Add($"{key} must be provided!"); 258 | } 259 | } 260 | 261 | return errs.Count == 0 ? null : "\r\n " + string.Join("\r\n ", errs); 262 | } 263 | 264 | public static void CopyDir(string src, string dest, string ignoreExt) 265 | { 266 | var srcDir = new DirectoryInfo(src); 267 | if (!srcDir.Exists) 268 | { 269 | Abort($"Source directory {src} does not exist"); 270 | } 271 | 272 | var destDir = new DirectoryInfo(dest); 273 | if (destDir.Exists) 274 | { 275 | Abort($"Destination directory {dest} already exists"); 276 | } 277 | 278 | try 279 | { 280 | destDir.Create(); 281 | } 282 | catch (Exception ex) 283 | { 284 | Abort(ex.Message); 285 | } 286 | 287 | var srcPath = srcDir.FullName; 288 | var destPath = destDir.FullName; 289 | 290 | foreach (var dir in Directory.EnumerateDirectories(srcPath, "*", SearchOption.AllDirectories)) 291 | { 292 | Directory.CreateDirectory(dir.Replace(srcPath, destPath)); 293 | } 294 | 295 | 296 | foreach (var file in Directory.EnumerateFiles(srcPath, "*", SearchOption.AllDirectories)) 297 | { 298 | var tarFile = file.Replace(srcPath, destPath); 299 | if (tarFile.EndsWith(ignoreExt)) 300 | { 301 | continue; 302 | } 303 | 304 | File.Copy(file, tarFile); 305 | } 306 | } 307 | 308 | public static bool Contains(this IEnumerable list, T e) 309 | { 310 | foreach (T el in list) 311 | { 312 | if (el.Equals(e)) 313 | { 314 | return true; 315 | } 316 | } 317 | 318 | return false; 319 | } 320 | 321 | public static Thread NewThread(Action action) 322 | { 323 | var th = new Thread(() => action()) 324 | { 325 | IsBackground = true 326 | }; 327 | th.Start(); 328 | return th; 329 | } 330 | 331 | public static long LastWriteTime(string filename) 332 | { 333 | var fileInfo = new FileInfo(filename); 334 | return fileInfo.Exists ? fileInfo.LastWriteTimeUtc.Ticks : 0; 335 | } 336 | 337 | public static string GetName(this Process proc) 338 | { 339 | if (proc.HasExited) 340 | { 341 | return $"Process-{proc.Id}"; 342 | } 343 | 344 | return $"Process-{proc.ProcessName}-{proc.Id}"; 345 | } 346 | 347 | private static void GetTree(this Process proc, List result) 348 | { 349 | result.Add(proc); 350 | 351 | var query = $"Select * From Win32_Process Where ParentProcessID={proc.Id}"; 352 | var searcher = new ManagementObjectSearcher(query); 353 | var moc = searcher.Get(); 354 | 355 | foreach (ManagementObject mo in moc) 356 | { 357 | var id = Convert.ToInt32(mo["ProcessID"]); 358 | var p = Process.GetProcessById(id); 359 | if (p == null) 360 | { 361 | continue; 362 | } 363 | 364 | p.GetTree(result); 365 | } 366 | } 367 | 368 | public static List GetTree(this Process proc) 369 | { 370 | var tree = new List(); 371 | proc.GetTree(tree); 372 | return tree; 373 | } 374 | 375 | public static void KillTree(this Process proc, Action print) 376 | { 377 | var tree = proc.GetTree(); 378 | var n = tree.Count; 379 | var names = new string[n]; 380 | 381 | for (var i = 0; i < n; i++) 382 | { 383 | names[i] = tree[i].GetName(); 384 | } 385 | 386 | for (var i = 0; i < n; i++) 387 | { 388 | var p = tree[i]; 389 | var name = names[i]; 390 | 391 | if (p.HasExited) 392 | { 393 | print($"{name} has exited already"); 394 | continue; 395 | } 396 | 397 | try 398 | { 399 | p.Kill(); 400 | print($"Killed {name}"); 401 | } 402 | catch (Exception ex) 403 | { 404 | print($"Failed to kill {name}: {ex.Message}"); 405 | } 406 | 407 | if (i != n - 1) 408 | { 409 | Thread.Sleep(1000); 410 | } 411 | } 412 | } 413 | 414 | public static float GetTreeMemory(this Process proc) 415 | { 416 | var tree = proc.GetTree(); 417 | var instanceNames = GetInstanceNames(); 418 | float mem = 0.0f; 419 | 420 | foreach (var p in tree) 421 | { 422 | if (p.HasExited) 423 | { 424 | continue; 425 | } 426 | 427 | mem += proc.GetMemory(instanceNames); 428 | } 429 | 430 | return mem; 431 | } 432 | 433 | public static float GetMemory(this Process proc, string[] instanceNames) 434 | { 435 | var cate = "Process"; 436 | var counter = "Working Set - Private"; 437 | var name = proc.GetInstanceName(instanceNames); 438 | 439 | if (name == null) 440 | { 441 | return 0; 442 | } 443 | 444 | using (var p = new PerformanceCounter(cate, counter, name, true)) 445 | { 446 | return p.NextValue() / 1024; 447 | } 448 | } 449 | 450 | public static string[] GetInstanceNames() 451 | { 452 | return new PerformanceCounterCategory("Process").GetInstanceNames(); 453 | } 454 | 455 | public static string GetInstanceName(this Process proc, string[] instanceNames) 456 | { 457 | foreach (var name in instanceNames) 458 | { 459 | if (!name.StartsWith(proc.ProcessName)) 460 | { 461 | continue; 462 | } 463 | 464 | using (var pfc = new PerformanceCounter("Process", "ID Process", name, true)) 465 | { 466 | if (proc.Id == (int) pfc.RawValue) 467 | { 468 | return name; 469 | } 470 | } 471 | } 472 | 473 | return null; 474 | } 475 | 476 | public static string GetData(this DataReceivedEventArgs ev) 477 | { 478 | string s = ev.Data; 479 | 480 | if (s == null) 481 | { 482 | return null; 483 | } 484 | 485 | int n = s.Length; 486 | if (n > 0 && s[n - 1] == '\0') 487 | { 488 | if (n == 1) 489 | { 490 | return null; 491 | } 492 | 493 | return s.Substring(0, n - 1); 494 | } 495 | 496 | return s; 497 | } 498 | 499 | public static string ToPrettyString(this Dictionary dict) 500 | { 501 | var n = dict.Count; 502 | var segs = new string[n]; 503 | var i = 0; 504 | 505 | foreach (var item in dict) 506 | { 507 | segs[i++] = $"[{item.Key}]=[{item.Value}]"; 508 | } 509 | 510 | return string.Join(", ", segs); 511 | } 512 | 513 | public static string ToPrettyTable(string[,] mat) 514 | { 515 | var m = mat.GetLength(0); 516 | var n = mat.GetLength(1); 517 | var w = new int[n + 1]; 518 | var buf = new string[n + 1]; 519 | var result = new string[2 * m + 1]; 520 | 521 | w[0] = (m - 1).ToString().Length; 522 | for (var j = 0; j < n; j++) 523 | { 524 | w[j + 1] = 1; 525 | for (var i = 0; i < m; i++) 526 | { 527 | w[j + 1] = Math.Max(w[j + 1], mat[i, j].Length); 528 | } 529 | } 530 | 531 | for (var j = 0; j <= n; j++) 532 | { 533 | buf[j] = "".PadRight(w[j], '-'); 534 | } 535 | 536 | result[0] = $"+-{string.Join("-+-", buf)}-+"; 537 | 538 | for (var i = 0; i < m; i++) 539 | { 540 | var si = (i == 0) ? "" : i.ToString(); 541 | buf[0] = si.PadRight(w[0], ' '); 542 | 543 | for (var j = 0; j < n; j++) 544 | { 545 | buf[j + 1] = mat[i, j].PadRight(w[j + 1], ' '); 546 | } 547 | 548 | result[2 * i + 1] = $"| {string.Join(" | ", buf)} |"; 549 | result[2 * i + 2] = result[0]; 550 | } 551 | 552 | return string.Join("\r\n", result); 553 | } 554 | 555 | public static bool InsertInto(List list, T el, Func isDepend) 556 | { 557 | int n = list.Count, i, j; 558 | 559 | for (i = n - 1; i >= 0; i--) 560 | { 561 | if (isDepend(el, list[i])) 562 | { 563 | break; 564 | } 565 | } 566 | 567 | if (i == -1) 568 | { 569 | list.Insert(0, el); 570 | return true; 571 | } 572 | 573 | for (j = 0; j < n; j++) 574 | { 575 | if (isDepend(list[j], el)) 576 | { 577 | break; 578 | } 579 | } 580 | 581 | if (j == n) 582 | { 583 | list.Add(el); 584 | return true; 585 | } 586 | 587 | if (i >= j) 588 | { 589 | return false; 590 | } 591 | 592 | list.Insert(j, el); 593 | return true; 594 | } 595 | 596 | public static bool MatchRegex(string input, string regex) 597 | { 598 | var rx = new Regex($"^{regex}$", RegexOptions.Compiled); 599 | return rx.IsMatch(input); 600 | } 601 | 602 | public static bool IsDigit(this string input) 603 | { 604 | return MatchRegex(input, @"\d+"); 605 | } 606 | } -------------------------------------------------------------------------------- /src/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.ServiceProcess; 4 | 5 | public class Program 6 | { 7 | private const string VERSION = "Easy Service: v1.0.11"; 8 | 9 | private const string USAGE = 10 | "Usage:\r\n" + 11 | " svc version|-v|--version\r\n" + 12 | " svc create $project-name\r\n" + 13 | " svc check|status [$project-directory]\r\n" + 14 | " svc test-worker [$project-directory]\r\n" + 15 | " svc install [$project-directory]\r\n" + 16 | " svc stop|start|remove [$project-directory|$service-name|$service-index|all]\r\n" + 17 | " svc restart [$project-directory|$service-name|$service-index]\r\n" + 18 | " svc list|ls\r\n" + 19 | "\r\n" + 20 | "Note: $project-directory must contain '\\' or '/'\r\n" + 21 | "\r\n" + 22 | "Documentation:\r\n" + 23 | " https://github.com/pandolia/easy-service"; 24 | 25 | public static int Main(string[] args) 26 | { 27 | if (args.Length == 0) 28 | { 29 | SimpleService.RunService(); 30 | return 0; 31 | } 32 | 33 | var op = args[0]; 34 | var opr = $"|{op}|"; 35 | var argc = args.Length - 1; 36 | var arg1 = argc == 1 ? args[1] : null; 37 | 38 | if (op == "version" || op == "--version" || op == "-v") 39 | { 40 | if (argc != 0) 41 | { 42 | Libs.Abort(USAGE); 43 | } 44 | 45 | Console.WriteLine(VERSION); 46 | return 0; 47 | } 48 | 49 | if (op == "create") 50 | { 51 | if (argc != 1) 52 | { 53 | Libs.Abort(USAGE); 54 | } 55 | 56 | CreateProject(arg1); 57 | return 0; 58 | } 59 | 60 | if (op == "list" || op == "ls") 61 | { 62 | if (argc != 0) 63 | { 64 | Libs.Abort(USAGE); 65 | } 66 | 67 | SvcUtils.ListAllEasyServices(); 68 | return 0; 69 | } 70 | 71 | var commands = "|check|status|test-worker|install|stop|start|remove|restart|log|"; 72 | 73 | if (!commands.Contains(opr) || argc > 1) 74 | { 75 | Libs.Abort(USAGE); 76 | } 77 | 78 | if (arg1 != null && (arg1.Contains('/') || arg1.Contains('\\'))) 79 | { 80 | if (!Directory.Exists(arg1)) 81 | { 82 | Libs.Abort($"Directory \"{arg1}\" not exists"); 83 | } 84 | 85 | Libs.SetCwd(arg1); 86 | arg1 = null; 87 | } 88 | 89 | if (op == "test-worker") 90 | { 91 | if (arg1 != null && arg1 != "--popup") 92 | { 93 | Libs.Abort($"Directory argument \"{arg1}\" should contain '/' or '\\'"); 94 | } 95 | 96 | TestWorker(arg1 == "--popup"); 97 | return 0; 98 | } 99 | 100 | if ("|check|status|install|".Contains(opr) && arg1 != null) 101 | { 102 | Libs.Abort($"Directory argument \"{arg1}\" should contain '/' or '\\'"); 103 | } 104 | 105 | if ("|restart|log|".Contains(opr) && arg1 == "all") 106 | { 107 | Libs.Abort(USAGE); 108 | } 109 | 110 | if (arg1 == null) 111 | { 112 | ManageOneByConfFile(op); 113 | } 114 | else if (arg1 != "all") 115 | { 116 | ManageOneBySvrIdentity(op, arg1); 117 | } 118 | else 119 | { 120 | SvcUtils.ManageAll(op); 121 | } 122 | 123 | return 0; 124 | } 125 | 126 | private static void ManageOneByConfFile(string op) 127 | { 128 | var conf = new Conf(); 129 | var serviceName = conf.ServiceName; 130 | var sc = SvcUtils.GetServiceController(serviceName); 131 | var status = (sc == null) ? "not installed" : sc.Status.ToString().ToLower(); 132 | 133 | if (op == "check" || op == "status") 134 | { 135 | conf.ShowConfig(); 136 | Console.WriteLine($"\r\nService status: {status}"); 137 | return; 138 | } 139 | 140 | if (op == "install") 141 | { 142 | if (sc != null) 143 | { 144 | Libs.Abort($"Service \"{serviceName}\" is already installed!"); 145 | } 146 | 147 | SvcUtils.InstallService(conf); 148 | return; 149 | } 150 | 151 | if (op == "log") 152 | { 153 | Libs.Abort("`log` command has been deprecated since v1.0.10"); 154 | } 155 | 156 | // op: start|stop|restart|remove 157 | 158 | if (sc == null) 159 | { 160 | Libs.Abort($"Service \"{serviceName}\" is not installed!"); 161 | } 162 | 163 | sc.Operate(op); 164 | } 165 | 166 | private static void ManageOneBySvrIdentity(string op, string arg1) 167 | { 168 | var info = SvcUtils.GetEasyService(arg1); 169 | var sc = info.Sc; 170 | 171 | info.Cd(); 172 | 173 | if (op == "log") 174 | { 175 | Libs.Abort("`log` command has been deprecated since v1.0.10"); 176 | } 177 | 178 | // op = start|stop|restart|remove 179 | sc.Operate(op); 180 | } 181 | 182 | private static void CreateProject(string arg1) 183 | { 184 | var err = Conf.CheckServiceName(arg1); 185 | if (err != null) 186 | { 187 | Libs.Abort($"[svc.critical] Bad project name `{arg1}`: {err}"); 188 | } 189 | 190 | Libs.CopyDir($"{Libs.BinDir}..\\samples\\csharp-version", arg1, ".log"); 191 | Libs.ReplaceStringInFile($"{arg1}\\svc.conf", "easy-service", arg1); 192 | Console.WriteLine($"Create an Easy-Service project in {arg1}"); 193 | } 194 | 195 | private static void TestWorker(bool popup) 196 | { 197 | var worker = new Worker(null, popup); 198 | worker.Start(); 199 | Console.ReadLine(); 200 | worker.Stop(); 201 | } 202 | } 203 | 204 | class SimpleService : ServiceBase 205 | { 206 | public static void RunService() 207 | { 208 | Run(new SimpleService()); 209 | } 210 | 211 | private Worker Worker = null; 212 | 213 | protected override void OnStart(string[] args) 214 | { 215 | SvcUtils.SetCwdInSvcBin(); 216 | Worker = new Worker(SvcUtils.AddLog); 217 | Worker.Start(); 218 | } 219 | 220 | protected override void OnStop() 221 | { 222 | Worker.Stop(); 223 | } 224 | } -------------------------------------------------------------------------------- /src/Main.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {41E81318-4181-4912-ACFA-D959AAE3E855} 6 | Exe 7 | Release 8 | AnyCPU 9 | 512 10 | true 11 | true 12 | prompt 13 | 4 14 | ..\bin\ 15 | svc 16 | 17 | 18 | 19 | packages\log4net.2.0.14\lib\net40\log4net.dll 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/MyFileLogger.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using log4net; 3 | using log4net.Appender; 4 | using log4net.Core; 5 | using log4net.Layout; 6 | using log4net.Repository.Hierarchy; 7 | 8 | class MyFileLogger 9 | { 10 | private readonly ILog Logger; 11 | 12 | public MyFileLogger(Conf conf) 13 | { 14 | var patternLayout = new PatternLayout 15 | { 16 | ConversionPattern = "%message%newline" 17 | }; 18 | patternLayout.ActivateOptions(); 19 | 20 | var fileAppender = new RollingFileAppender 21 | { 22 | Name = "MyFileAppenderName", 23 | Encoding = Encoding.UTF8, 24 | RollingStyle = RollingFileAppender.RollingMode.Date, 25 | File = conf.OutFileDir, 26 | DatePattern = "yyyy-MM-dd'.log'", 27 | StaticLogFileName = false, 28 | AppendToFile = true, 29 | Layout = patternLayout 30 | }; 31 | fileAppender.ActivateOptions(); 32 | 33 | var hierarchy = (Hierarchy)LogManager.GetRepository(); 34 | hierarchy.Root.AddAppender(fileAppender); 35 | hierarchy.Root.Level = Level.All; 36 | hierarchy.Configured = true; 37 | 38 | Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); 39 | } 40 | 41 | public void Log(string line) 42 | { 43 | Logger.Info(line); 44 | } 45 | } -------------------------------------------------------------------------------- /src/SampleWorker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | class Program 5 | { 6 | public static bool running; 7 | 8 | public static void Log(string s) 9 | { 10 | Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {s}"); 11 | } 12 | 13 | public static void Loop() 14 | { 15 | Log("Started SampleWorker(c# version), press Enter to exit"); 16 | while (running) 17 | { 18 | Log("Running"); 19 | Thread.Sleep(1000); 20 | } 21 | Log("Stopped SampleWorker(c# version)"); 22 | } 23 | 24 | public static void Main() 25 | { 26 | var th = new Thread(Loop); 27 | 28 | running = true; 29 | th.Start(); 30 | 31 | var msg = Console.ReadLine(); 32 | Log($"Received message \"{msg}\" from the Monitor"); 33 | 34 | running = false; 35 | th.Join(); 36 | } 37 | } -------------------------------------------------------------------------------- /src/SampleWorker.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {D4F448E9-9122-4A63-BB3E-1996CD01D7FE} 6 | Exe 7 | Release 8 | AnyCPU 9 | 512 10 | true 11 | true 12 | prompt 13 | 4 14 | ..\samples\csharp-version\worker\ 15 | sample-worker 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/SvcUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Management; 6 | using System.ServiceProcess; 7 | 8 | public static class SvcUtils 9 | { 10 | public static void InstallService(Conf conf) 11 | { 12 | var depend = conf.Dependencies.AddUniq(Consts.NECESSARY_DEPENDENCY, '/'); 13 | 14 | // add "[svc]" to DisplayName to avoid "ServiceName == DisplayName" (which cases error in win7) 15 | var scArgs = $"create \"{conf.ServiceName}\" binPath= \"{Libs.BinPath}\" start= auto " 16 | + $"DisplayName= \"[svc] {conf.DisplayName}\" depend= \"{depend}\""; 17 | 18 | if (conf.User.Length > 0) 19 | { 20 | var obj = (conf.Domain.Length > 0) ? $"{conf.Domain}\\{conf.User}" : conf.User; 21 | scArgs = $"{scArgs} obj= \"{obj}\" password= \"{conf.Password}\""; 22 | } 23 | 24 | Libs.Exec("sc", scArgs); 25 | 26 | var sc = GetServiceController(conf.ServiceName); 27 | if (sc == null) 28 | { 29 | Libs.Abort($"Failed to install Service \"{conf.ServiceName}\""); 30 | } 31 | 32 | var msg = $"Installed Service \"{conf.ServiceName}\""; 33 | Console.WriteLine(msg); 34 | AddLog("INFO", msg, false); 35 | AddLog("INFO", ""); 36 | 37 | SetSvcDescription(conf); 38 | 39 | sc.StartService(); 40 | } 41 | 42 | public static void Operate(this ServiceController sc, string op) 43 | { 44 | switch (op) 45 | { 46 | case "start": 47 | sc.StartService(); 48 | break; 49 | 50 | case "stop": 51 | sc.StopService(); 52 | break; 53 | 54 | case "restart": 55 | sc.RestartService(); 56 | break; 57 | 58 | case "remove": 59 | sc.RemoveService(); 60 | break; 61 | } 62 | } 63 | 64 | public static void RemoveService(this ServiceController sc) 65 | { 66 | var name = sc.ServiceName; 67 | 68 | sc.StopService(); 69 | 70 | Libs.Exec("sc", $"delete \"{name}\""); 71 | 72 | if (GetServiceController(name) != null) 73 | { 74 | Libs.Abort($"Failed to remove Service \"{name}\""); 75 | } 76 | 77 | var msg = $"Removed Service \"{name}\""; 78 | AddLog("INFO", ""); 79 | AddLog("INFO", msg); 80 | Console.WriteLine(msg); 81 | } 82 | 83 | public static void StartService(this ServiceController sc) 84 | { 85 | if (sc.Status == ServiceControllerStatus.Running) 86 | { 87 | Console.WriteLine($"Service \"{sc.ServiceName}\" is already started"); 88 | return; 89 | } 90 | 91 | var startMsg = $"Starting service \"{sc.ServiceName}\""; 92 | 93 | var timeoutMsg = $"Failed to start the service, " 94 | + $"please refer to {Libs.GetCwd()}svc.log or " 95 | + $"{Libs.BinDir}*.error.txt to see what happened"; 96 | 97 | var okMsg = $"Started Service \"{sc.ServiceName}\""; 98 | 99 | Libs.Timeout(sc.StartSvc, Consts.MAX_SERVICE_START_STOP_SECONDS, startMsg, timeoutMsg, okMsg); 100 | } 101 | 102 | public static void StopService(this ServiceController sc) 103 | { 104 | if (sc.Status == ServiceControllerStatus.Stopped) 105 | { 106 | Console.WriteLine($"Service \"{sc.ServiceName}\" is already stopped"); 107 | return; 108 | } 109 | 110 | var startMsg = $"Stopping service \"{sc.ServiceName}\""; 111 | 112 | var timeoutMsg = $"Failed to stop the service, " 113 | + $"please refer to {Libs.GetCwd()}svc.log to see what happened"; 114 | 115 | var okMsg = $"Stopped Service \"{sc.ServiceName}\""; 116 | 117 | Libs.Timeout(sc.StopSvc, Consts.MAX_SERVICE_START_STOP_SECONDS, startMsg, timeoutMsg, okMsg); 118 | } 119 | 120 | public static void RestartService(this ServiceController sc) 121 | { 122 | sc.StopService(); 123 | sc.StartService(); 124 | } 125 | 126 | private static void StartSvc(this ServiceController sc) 127 | { 128 | sc.Start(); 129 | sc.WaitForStatus(ServiceControllerStatus.Running); 130 | sc.Refresh(); 131 | } 132 | 133 | private static void StopSvc(this ServiceController sc) 134 | { 135 | sc.Stop(); 136 | sc.WaitForStatus(ServiceControllerStatus.Stopped); 137 | sc.Refresh(); 138 | } 139 | 140 | private static void SetSvcDescription(Conf conf) 141 | { 142 | var description = $"{conf.Description} @<{Libs.GetCwd()}>"; 143 | var scArgs = $"description \"{conf.ServiceName}\" \"{description}\""; 144 | string err; 145 | 146 | Libs.Exec("sc", scArgs); 147 | try 148 | { 149 | var mngObj = GetServiceManagementObjectByName(conf.ServiceName); 150 | if (mngObj != null && mngObj["Description"].ToString() == description) 151 | { 152 | return; 153 | } 154 | 155 | err = "unknown reason"; 156 | } 157 | catch (Exception ex) 158 | { 159 | err = ex.ToString(); 160 | } 161 | 162 | err = $"Failed to set description for Service \"{conf.ServiceName}\": {err}"; 163 | AddLog("ERROR", err); 164 | Libs.Abort($"{err}\r\nPlease run `svc remove` to remove the Service"); 165 | } 166 | 167 | public static void SetCwdInSvcBin() 168 | { 169 | try 170 | { 171 | var description = GetServiceDescriptionInSvcBin(); 172 | var cwd = GetCwdInDescription(description); 173 | Libs.SetCwd(cwd); 174 | } 175 | catch (Exception ex) 176 | { 177 | Libs.Dump($"Failed to set cwd in service's bin, {ex}"); 178 | Environment.Exit(1); 179 | } 180 | } 181 | 182 | private static string GetCwdInDescription(string description) 183 | { 184 | var i = description.LastIndexOf('<') + 1; 185 | var n = description.Length - 1 - i; 186 | var cwd = description.Substring(i, n); 187 | return cwd; 188 | } 189 | 190 | public static ServiceController GetServiceController(string name) 191 | { 192 | var sc = new ServiceController(name); 193 | try 194 | { 195 | _ = sc.ServiceName; 196 | } 197 | catch (Exception) 198 | { 199 | sc = null; 200 | } 201 | 202 | return sc; 203 | } 204 | 205 | private static ManagementObject GetServiceManagementObjectByName(string name) 206 | { 207 | using (var mngObj = new ManagementObject($"Win32_Service.Name='{name}'")) 208 | { 209 | mngObj.Get(); 210 | return mngObj; 211 | } 212 | } 213 | 214 | private static string GetServiceDescriptionInSvcBin() 215 | { 216 | int processId = Process.GetCurrentProcess().Id; 217 | string query = $"SELECT * FROM Win32_Service where ProcessId = {processId}"; 218 | using (var searcher = new ManagementObjectSearcher(query)) 219 | { 220 | foreach (ManagementObject queryObj in searcher.Get()) 221 | { 222 | return queryObj["Description"].ToString(); 223 | } 224 | } 225 | 226 | throw new Exception("Can't get the service management object"); 227 | } 228 | 229 | public static void ListAllEasyServices() 230 | { 231 | ServiceInfo.PrintInfoList(GetEasyServices()); 232 | } 233 | 234 | private static List GetEasyServices() 235 | { 236 | Console.Write("Getting all EasyServices, please wait..."); 237 | Console.Out.Flush(); 238 | 239 | var hasCircle = false; 240 | var infoList = new List(); 241 | foreach (var sc in ServiceController.GetServices()) 242 | { 243 | var name = sc.ServiceName; 244 | var mngObj = GetServiceManagementObjectByName(name); 245 | var path = mngObj["PathName"].ToString(); 246 | if (path != Libs.BinPath) 247 | { 248 | continue; 249 | } 250 | 251 | var description = mngObj["Description"].ToString(); 252 | var confDir = GetCwdInDescription(description); 253 | var info = new ServiceInfo(sc, confDir); 254 | 255 | if (!Libs.InsertInto(infoList, info, ServiceInfo.IsDepend)) 256 | { 257 | Console.WriteLine("\r\n[ERROR] Circle dependencies detected"); 258 | infoList.Add(info); 259 | hasCircle = true; 260 | } 261 | } 262 | 263 | if (!hasCircle) 264 | { 265 | Console.Write($"\r{"".PadRight(60)}\r"); 266 | Console.Out.Flush(); 267 | } 268 | 269 | if (infoList.Count == 0) 270 | { 271 | Libs.Abort($"No EasyService found"); 272 | } 273 | 274 | return infoList; 275 | } 276 | 277 | public static ServiceInfo GetEasyService(string nameOrIndex) 278 | { 279 | var infoList = GetEasyServices(); 280 | 281 | if (int.TryParse(nameOrIndex, out int i)) 282 | { 283 | if (i < 1 || i > infoList.Count) 284 | { 285 | Libs.Abort($"Index \"{i}\" out of boundary 1~\"{infoList.Count}\""); 286 | } 287 | 288 | return infoList[i - 1]; 289 | } 290 | 291 | foreach (var info in infoList) 292 | { 293 | if (info.Sc.ServiceName == nameOrIndex) 294 | { 295 | return info; 296 | } 297 | } 298 | 299 | Libs.Abort($"EasyService \"{nameOrIndex}\" not exists"); 300 | return null; 301 | } 302 | 303 | // op: start|stop|remove 304 | public static void ManageAll(string op) 305 | { 306 | var infoList = GetEasyServices(); 307 | 308 | if (op != "start") 309 | { 310 | infoList.Reverse(); 311 | } 312 | 313 | ServiceInfo.PrintInfoList(infoList); 314 | 315 | foreach (var info in infoList) 316 | { 317 | info.Cd(); 318 | info.Sc.Refresh(); 319 | info.Sc.Operate(op); 320 | } 321 | 322 | if (op != "remove") 323 | { 324 | ServiceInfo.PrintInfoList(infoList); 325 | } 326 | else 327 | { 328 | Console.WriteLine("\r\nAll EasyServices are removed\r\n"); 329 | } 330 | } 331 | 332 | public static string GetName(this ServiceController sc) 333 | { 334 | try 335 | { 336 | return sc.ServiceName; 337 | } 338 | catch (InvalidOperationException) 339 | { 340 | return null; 341 | } 342 | } 343 | 344 | public static string GetDependenciesString(this ServiceController sc) 345 | { 346 | var dependencies = sc.ServicesDependedOn; 347 | var n = dependencies.Length; 348 | var buf = new string[n]; 349 | var j = 0; 350 | 351 | for (var i = 0; i < n; i++) 352 | { 353 | var name = dependencies[i].GetName(); 354 | if (name == Consts.NECESSARY_DEPENDENCY || name == null) 355 | { 356 | continue; 357 | } 358 | 359 | buf[j++] = name; 360 | } 361 | 362 | return string.Join(",", buf, 0, j); 363 | } 364 | 365 | private static readonly object LogLock = new object(); 366 | 367 | private static void AddLog(string level, string s, bool append) 368 | { 369 | s = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] {s}"; 370 | 371 | lock (LogLock) 372 | { 373 | try 374 | { 375 | Libs.WriteLineToFile(Consts.LOG_FILE, s, append); 376 | } 377 | catch (Exception e) 378 | { 379 | Libs.Dump(e.Message); 380 | } 381 | } 382 | } 383 | 384 | public static void AddLog(string level, string s) 385 | { 386 | AddLog(level, s, true); 387 | } 388 | } 389 | 390 | public class ServiceInfo 391 | { 392 | public readonly ServiceController Sc; 393 | 394 | public readonly string ConfDir; 395 | 396 | public ServiceInfo(ServiceController sc, string confDir) 397 | { 398 | Sc = sc; 399 | ConfDir = confDir; 400 | } 401 | 402 | public static bool IsDepend(ServiceInfo si1, ServiceInfo si2) 403 | { 404 | var name2 = si2.Sc.GetName(); 405 | 406 | if (name2 == null) 407 | { 408 | return false; 409 | } 410 | 411 | foreach (var sc in si1.Sc.ServicesDependedOn) 412 | { 413 | var name = sc.GetName(); 414 | 415 | if (name == null) 416 | { 417 | continue; 418 | } 419 | 420 | if (name2 == name) 421 | { 422 | return true; 423 | } 424 | } 425 | 426 | return false; 427 | } 428 | 429 | public static void PrintInfoList(List infoList) 430 | { 431 | var m = infoList.Count; 432 | 433 | var mat = new string[m + 1, 4]; 434 | 435 | mat[0, 0] = "Service"; 436 | mat[0, 1] = "Status"; 437 | mat[0, 2] = "Dependencies"; 438 | mat[0, 3] = "Config Directory"; 439 | 440 | for (var i = 0; i < m; i++) 441 | { 442 | var info = infoList[i]; 443 | info.Sc.Refresh(); 444 | mat[i + 1, 0] = info.Sc.ServiceName; 445 | mat[i + 1, 1] = info.Sc.Status.ToString().ToLower(); 446 | mat[i + 1, 2] = info.Sc.GetDependenciesString(); 447 | mat[i + 1, 3] = info.ConfDir; 448 | } 449 | 450 | Console.WriteLine($"\r\n{Libs.ToPrettyTable(mat)}\r\n"); 451 | } 452 | 453 | public void Cd() 454 | { 455 | if (!Directory.Exists(ConfDir)) 456 | { 457 | Libs.Abort($"Service directory \"{ConfDir}\" not exists"); 458 | } 459 | 460 | Libs.SetCwd(ConfDir); 461 | } 462 | } -------------------------------------------------------------------------------- /src/Worker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | 5 | public class Worker 6 | { 7 | private readonly Conf Conf; 8 | 9 | private readonly Action AddLog; 10 | 11 | private readonly bool RedirectMode; 12 | 13 | private readonly ProcessStartInfo Psi; 14 | 15 | private readonly object ProcLock = new object(); 16 | 17 | private readonly MyFileLogger FileLogger = null; 18 | 19 | private Process Proc = null; 20 | 21 | public Worker(Action logAdder = null, bool popup = false) 22 | { 23 | AddLog = logAdder ?? Print; 24 | 25 | Conf = new Conf(Abort); 26 | 27 | if (Conf.OutFileDir != null) 28 | { 29 | FileLogger = new MyFileLogger(Conf); 30 | } 31 | 32 | Psi = new ProcessStartInfo 33 | { 34 | FileName = Conf.WorkerFileName, 35 | Arguments = Conf.WorkerArguments, 36 | WorkingDirectory = Conf.WorkingDir 37 | }; 38 | 39 | if (popup) 40 | { 41 | RedirectMode = false; 42 | Psi.UseShellExecute = true; 43 | return; 44 | } 45 | 46 | RedirectMode = true; 47 | Psi.UseShellExecute = false; 48 | Psi.RedirectStandardOutput = true; 49 | Psi.RedirectStandardError = true; 50 | Psi.RedirectStandardInput = true; 51 | Psi.StandardErrorEncoding = Conf.WorkerEncodingObj; 52 | Psi.StandardOutputEncoding = Conf.WorkerEncodingObj; 53 | 54 | foreach (var item in Conf.Environments) 55 | { 56 | Psi.EnvironmentVariables[item.Key] = item.Value; 57 | } 58 | } 59 | 60 | public void Start() 61 | { 62 | Proc = new Process 63 | { 64 | StartInfo = Psi 65 | }; 66 | 67 | if (RedirectMode) 68 | { 69 | Proc.OutputDataReceived += OutPut; 70 | Proc.ErrorDataReceived += OutPut; 71 | } 72 | 73 | Proc.Exited += OnExit; 74 | Proc.EnableRaisingEvents = true; 75 | 76 | try 77 | { 78 | Proc.Start(); 79 | } 80 | catch (Exception ex) 81 | { 82 | Abort($"Failed to create Worker `{Conf.Worker}` in `{Conf.WorkingDir}`:\r\n{ex}"); 83 | } 84 | 85 | Info($"Created Worker `{Conf.Worker}` in `{Conf.WorkingDir}`"); 86 | 87 | if (RedirectMode) 88 | { 89 | Proc.BeginErrorReadLine(); 90 | Proc.BeginOutputReadLine(); 91 | } 92 | 93 | if (Conf.WorkerMemoryLimit != -1) 94 | { 95 | Libs.NewThread(MonitorMemory); 96 | } 97 | } 98 | 99 | private void MonitorMemory() 100 | { 101 | Info("Start Memory Monitor Loop"); 102 | 103 | while (true) 104 | { 105 | Thread.Sleep(Consts.MONITOR_INTERVAL_MINUTES * 60 * 1000); 106 | 107 | lock (ProcLock) 108 | { 109 | if (Proc == null) 110 | { 111 | break; 112 | } 113 | 114 | var mem = Proc.GetTreeMemory() / 1024; 115 | if (mem < Conf.WorkerMemoryLimit) 116 | { 117 | continue; 118 | } 119 | 120 | Warn($"Worker's memory({mem:N3}MB) exceeds {Conf.WorkerMemoryLimit}MB"); 121 | Proc.KillTree(Info); 122 | break; 123 | } 124 | } 125 | } 126 | 127 | public void Stop() 128 | { 129 | lock (ProcLock) 130 | { 131 | var proc = Proc; 132 | Proc = null; 133 | 134 | if (RedirectMode && Conf.WaitSecondsForWorkerToExit > 0 && NotifyToExit(proc)) 135 | { 136 | return; 137 | } 138 | 139 | proc.KillTree(Info); 140 | } 141 | } 142 | 143 | private bool NotifyToExit(Process proc) 144 | { 145 | try 146 | { 147 | proc.StandardInput.Write("exit\r\n"); 148 | proc.StandardInput.Flush(); 149 | Info("Notified Worker to exit"); 150 | } 151 | catch (Exception ex) 152 | { 153 | Error($"Failed to notify Worker to exit:\r\n{ex}"); 154 | return false; 155 | } 156 | 157 | if (!proc.WaitForExit(Conf.WaitSecondsForWorkerToExit * 1000)) 158 | { 159 | Info("Worker refused to exit"); 160 | return false; 161 | } 162 | 163 | Info($"Worker exited with code {proc.ExitCode}"); 164 | return true; 165 | } 166 | 167 | private void OnExit(object sender, EventArgs e) 168 | { 169 | lock (ProcLock) 170 | { 171 | if (Proc == null) 172 | { 173 | return; 174 | } 175 | 176 | Warn($"The worker exited without be notified, " 177 | + $"it will be re-created after {Consts.RESTART_WAIT_SECONDS} seconds"); 178 | 179 | Thread.Sleep(Consts.RESTART_WAIT_SECONDS * 1000); 180 | Start(); 181 | Thread.Sleep(1000); 182 | } 183 | } 184 | 185 | private void OutPut(object sender, DataReceivedEventArgs ev) 186 | { 187 | var data = ev.GetData(); 188 | if (data == null) 189 | { 190 | return; 191 | } 192 | 193 | if (AddLog == Print) 194 | { 195 | Console.WriteLine(data); 196 | return; 197 | } 198 | 199 | if (FileLogger == null) 200 | { 201 | return; 202 | } 203 | 204 | FileLogger.Log(data); 205 | } 206 | 207 | private static void Print(string level, string s) 208 | { 209 | Console.WriteLine($"[svc.{level.ToLower()}] {s}"); 210 | } 211 | 212 | private void Info(string msg) 213 | { 214 | AddLog("INFO", msg); 215 | } 216 | 217 | private void Warn(string msg) 218 | { 219 | AddLog("WARN", msg); 220 | } 221 | 222 | private void Error(string msg) 223 | { 224 | AddLog("ERROR", msg); 225 | } 226 | 227 | private void Abort(string msg) 228 | { 229 | AddLog("CRITICAL", msg); 230 | Environment.Exit(1); 231 | } 232 | } -------------------------------------------------------------------------------- /src/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------