本章的主题为用Python实现一个Windows上的服务,我们打开自己电脑上的服务程序,可以看到系统后台启动了很多服务。这些服务在默默的为我们提供了很多功能,可以说我们作为用户,对其看不到摸不着。如果我们也想让自己编写的程序,以服务这种形式在计算机的后台默默的运行,那该怎么做呢?本章就将围绕这个主题进行学习。
15.1 要解决什么问题
我们要解决的问题就是如何用Python语言编写一个可以在Windows上运行的服务,就像Windows系统自身的一些服务程序一样,一直默默的在后台运行,而用户感知不到。那我们这里有几个问题需要解决:
- · 用Python语言实现一个服务程序;
- · 该服务程序可以像Windows上的服务一样支持install/start/stop/remove操作;
- · 将其打包为exe文件,可以在命令行进行执行上述操作;
- · 将其打包为安装文件,在windows上安装后,服务即可被安装并服务开始运行;
- · 安装后的服务被卸载时,停止服务,并删除服务;
如果能解决上述几个问题,那么我们用Python实现的服务将和Windows自身的服务没什么区别。这样我们就可以将那些需要电脑每次启动后立即在后台执行的程序都实现为服务,就会非常的方便实用。比如我们将前面学过的自动连接Wifi章节的内容实现为服务,因为实际上电脑开机后就需要自动连接Wifi,开机后每次都手动执行就比较麻烦。所以该功能更适合实现为服务,这里就以此为例来介绍如何实现Windows服务程序。
15.2 实现思路
我们这里的实现思路就是比较简单,由于是实现Windows上的服务程序,因此比较借助win32serviceutil.ServiceFramework的框架进行实现。这应该也是Python的一大魅力所在吧,我们无须了解win32serviceuntil的具体细节,只需要简单的使用它来实现我们的服务程序即可。我们可以将更多的精力放在我们的程序功能上,而无须花费时间和精力在我们不关心的事情上。
win32serviceutil.ServiceFramework模块提供了一些变量名和接口,对于我们来说只需要将相应的变量名赋值为我们自己的内容,并将相关接口进行实现即可。实现了我们的服务类之后,只需要调用特定servicemanager库进行必要的初始化,就可以实现一个服务程序了。
实现很简单,也正是简单,所以才使用Python去解决我们的问题,提高我们的效率。接下来,将介绍代码实现中会使用到模块和库。
15.3 相关模块的安装及介绍
本节将会介绍我们需要用到的模块或者库。
15.3.1 win32serviceutil模块
win32serviceutil模块是一个用于编写windows服务的模块,为我们提供了固定的框架和接口,便于我们容易的用Python代码编写出自己的windows服务程序。具体使用参考我们的后面的代码实现,因为必须按固定的框架编写,所以这里不赘述。
15.3.2 win32service模块
win32service模块也是编写windows服务程序时需要用到的一个模块,在我们的程序中,只用到了服务停止宏,用于告诉服务控制管理器,当前服务将被停止。具体使用参考我们的后面的代码实现,这里不赘述。
15.3.3 win32event模块
win32event模块用于创建一个事件,是服务程序需要支持的特性,我们的程序中没有使用到。不过为了完整性,还是创建了一个事件,但没有做具体的事情。
15.3.4 win32timezone模块
win32timezone模块用于处理时间相关的事情,在我们注册服务的时候,会根据操作系统的时区处理时间相关的信息。虽然在程序中没有显示的使用该模块,但在安装服务的过程中会使用到该模块,详细的信息可以参考下面的链接。
15.3.5 winerror模块
winerror模块,在我们调试,或者服务程序出差时操作系统给给出一些错误码和提示信息,而winerror模块就跟操作系统的错误码有关。
15.3.6 servicemanager模块
servicemanager服务管理模块,负责系统中的服务程序管理事务。在我们的程序中,其负责初始化工作,为安装我们的服务做准备工作,并启动服务控制器。
15.3.7 sys模块
sys模块是一个内建模块,不需要单独安装。
sys模块提供了对Python解释器使用的一些变量的访问,并可以进行一些修改,例如对环境变量PATH的读取和修改,并提供了某些和解释器进行交互的函数以使我们的程序能够和解释器进行交互。
例如,sys.argv会将命令行参数以list的形式传递给Python脚本,sys.argv[0]是脚本的名字,sys.argv[1]是第一个参数,以此类推。
sys.exit()表示退出程序,也可以带参数表示退出码,如果有其他程序调用该程序,即可以通过返回的数字来确定被调用程序的退出原因。
sys.implementation查看当前正在运行的Python解释器的版本信息。
>>> sys.implementationnamespace(cache_tag='cpython-36', hexversion=50726384, name='cpython', version=sys.version_info(major=3, minor=6, micro=5, releaselevel='final', serial=0))>>>
sys.stdin,sys.stddout,sys.stderr解释器用于标准输入,标准输出和错误。
15.3.8 time模块
time模块提供了时间相关的各种函数。
time.asctime()函数可以将结构体struct_time所代表的时间转换为这样的字符串'Sun Jun 19 13:31:15 1994'。我们可以通过time.localtime()函数得到结构体struct_time。
time.sleep()函数的入参单位是秒,如果线程需要被挂起,可以通过调用此函数达到该目的。这里的入参也可以是小数,表示更精确的睡眠时间。我们将会使用该函数进行必要的等待以确保另一件事情结束。
time.strftime()函数也用于格式化时间。
>>> import time>>> time.strftime('%Y%m%d',time.localtime(time.time()))'20190714'>>>>>> time.strftime('%Y-%m-%d')'2019-07-14'>>>
15.3.9 subprocess模块
subprocess模块,我们可以利用该模块创建一个新的进程。如果需要的话,可以通过入参连接到程序的输入,输出和错误管道,并通过相应的管道获取程序的输入,输出和错误返回。
subprocess.run()函数可以运行由参数组成的命令的字符串,并且直到命令完成,在完成后返回一个完成实例。
>>> import subprocess>>> subprocess.run("ping -n 1 www.baidu.com", shell=True)正在 Ping www.a.shifen.com [180.97.33.108] 具有 32 字节的数据:来自 180.97.33.108 的回复: 字节=32 时间=33ms TTL=55180.97.33.108 的 Ping 统计信息:数据包: 已发送 = 1,已接收 = 1,丢失 = 0 (0% 丢失),往返行程的估计时间(以毫秒为单位):最短 = 33ms,最长 = 33ms,平均 = 33msCompletedProcess(args='ping -n 1 www.baidu.com', returncode=0)>>>
subprocess.PIPE代表管道是一个特殊的值,使用该值可以将标准流的管道打开。打开后可以通过sys.stdin获取输入,通过sys.stdout读出输出结果,同时也可以通过sys.stderr得到错误输出。
subprocess.Popen()在新进程中执行子程序,在本书中我们将会多次使用该接口,某些场景下我们不需要等待被调用程序执行完成或返回,只需要将其启动即可。在预订测试线章节的例子中,我们将使用该接口去启动升级切换程序,启动后无需等待其返回。如果需要等待执行完成,则只需要调用wait()接口等待其完成即可。
import subprocess, osp = subprocess.Popen("dir", cwd=os.getcwd(), shell=True,stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)p.wait()
如上面代码中使用的一样,wait()等待子程序结束。同时可以设置等待超时时间。
更多的细节可以参考如下链接。
15.3.10 logging模块
logging模块是一个非常好用的日志模块,支持将日志信息显示到标准输出即常说的串口,也支持将信息保存到日志文件中。此外该模块也提供了很好的日志分级机制,支持debug,info,warning,error和critical五个级别,该模块会根据您设置的级别将该级别以上的日志存储到日志中。还可以自定义日志信息的格式,下面的代码片段展示了如何使用日志模块。
import logginglogging.basicConfig(level=logging.INFO,format="%(asctime)s %(name)s %(levelname)s %(message)s",datefmt='%Y-%m-%d %H:%M:%S %a',filename='WifiOnlineService.log',filemode='a')logging.debug("WifiOnlineService: %s", '__init__')logging.info("WifiOnlineService: %s", 'isAlive')
15.4 代码实现
接下来,我们将开始实现具体代码,将思想变为现实。
15.4.1 编写伪码
按照惯例,我们首先编写一定的伪码,来帮助我们从整体上把握程序的结构。
#利用logging模块定义日志文件及消息结构#实现我们的服务类基于win32serviceutil.ServiceFrameworkclass WifiOnlineService(win32serviceutil.ServiceFramework):#服务名#服务显示名称#服务描述#实现运行接口def SvcDoRun(self):#具体的服务功能代码实现在这里#实现停止接口def SvcStop(self):#停止服务时需要做到事情都在这里实现if __name__ == '__main__':#调用servicemanager进行相关的初始化及准备工作#调用HandleCommandLine接口,使我们的服务支持命令行,比如install/start等参数
从我们的伪码可以看出,代码框架比较简单。
15.4.2 Python代码实现
具体的代码实现来了,详细如下,具体功能很简单就是每3秒ping一次百度。
import win32serviceutilimport win32serviceimport win32eventimport win32timezoneimport winerrorimport servicemanagerimport sysimport timefrom subprocess import Popenimport logginglogging.basicConfig(level=logging.INFO,format="%(asctime)s %(name)s %(levelname)s %(message)s",datefmt='%Y-%m-%d %H:%M:%S %a',filename='WifiOnlineService.log',filemode='a')class WifiOnlineService(win32serviceutil.ServiceFramework):# 服务名_svc_name_ = "WifiOnline"# 服务显示名称_svc_display_name_ = "WifiOnline"# 服务描述_svc_description_ = "Making sure wifi always works"def __init__(self, args):win32serviceutil.ServiceFramework.__init__(self, args)self.eWaitStop = win32event.CreateEvent(None, 0, 0, None)self.isAlive = Truelogging.info("WifiOnlineService: %s", '__init__')def SvcDoRun(self):logging.info("WifiOnlineService: %s", 'SvcDoRun')while self.isAlive:logging.info("WifiOnlineService: %s", 'isAlive')time.sleep(3)Popen("ping -n 1 www.baidu.com", shell=True)def SvcStop(self):logging.info("WifiOnlineService: %s", 'SvcStop')# 先告诉SCM停止这个过程self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)# 设置事件win32event.SetEvent(self.eWaitStop)self.isAlive = Falseif __name__ == '__main__':if len(sys.argv) == 1:# 服务程序应该会先调用一次初始化logging.info("WifiOnlineService: %s", '1')servicemanager.Initialize()servicemanager.PrepareToHostSingle(WifiOnlineService)servicemanager.StartServiceCtrlDispatcher()else:win32serviceutil.HandleCommandLine(WifiOnlineService)
由于服务是运行在后台,看不到摸不着,因此需要通过日志来观察其运行过程。
2019-09-06 00:27:16 Fri root INFO WifiOnlineService: 12019-09-06 00:27:16 Fri root INFO WifiOnlineService: __init__2019-09-06 00:27:16 Fri root INFO WifiOnlineService: SvcDoRun2019-09-06 00:27:16 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:19 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:22 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:25 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:28 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:31 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:34 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:37 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:40 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:43 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:46 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:49 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:52 Fri root INFO WifiOnlineService: isAlive2019-09-06 00:27:54 Fri root INFO WifiOnlineService: SvcStop
上面的日志显示,先进行的初始化,然后执行了启动服务的操作,运行了一段时间后执行了停止操作。通过时间戳还可以看出,我们的服务每3秒执行一次ping操作。需要注意的是,我们这里的Python代码需要打包为exe再去做服务的安装和启动。直接基于Python代码去安装服务的时候没有问题,但当启动服务的时候会有问题。具体原因可以通过日志文件看出来,当我们用Python代码去执行install操作时不会执行if语句中的内容,从而导致启动时有问题,因此建议打包为exe再去操作。或者先不带任何参数,单独执行一次Python代码,然后再带install参数进行安装,再启动服务。
15.4.3 打包为exe进行发布
接下来,我们需要将Python代码进行打包,生成exe进行方便使用。
将Python代码转换为exe程序,我们使用的是pyinstaller,具体的转换代码我实现为python代码。只需要执行下面的Python代码,就会调用pyinstaller生成一个单独的exe文件。这种形式的优点是只有一个exe文件,其他所有的依赖的Python环境文件都被打包在该exe文件中。而缺点就是由此导致该文件比较大,而且每次执行都相当于有一个解压到临时目录的过程,所以执行比较慢。
import os, shutilfrom subprocess import Popenif os.path.exists('dist'):shutil.rmtree("dist")if os.path.exists('build'):shutil.rmtree("build")if os.path.exists('__pycache__'):shutil.rmtree("__pycache__")handle = Popen("pyinstaller -F -w -n wifi_online_service wifi_online_service.py")handle.wait()shutil.copyfile("dist/wifi_online_service.exe", "wifi_online_service.exe")shutil.rmtree("dist")shutil.rmtree("build")shutil.rmtree("__pycache__")
个人推荐将其打包为目录,而不是单个exe文件。这样的好处就是不需要每次执行都进行解压到临时目录,而是直接在同目录下调用执行,因此比较快。而为了更方便,我们这里会介绍如何将该目录打包为安装文件,这样在给别人用的时候,只需要给一个安装文件。用户拿到该安装文件后,安装上之后就可以使用了,且使用感受更好。
下面就是打包为目录的代码,跟上面有区别,其中一个打包参数为-D。
import os, shutilfrom subprocess import Popenif os.path.exists('dist'):shutil.rmtree("dist")if os.path.exists('build'):shutil.rmtree("build")if os.path.exists('__pycache__'):shutil.rmtree("__pycache__")handle = Popen("pyinstaller -D -w -n wifi_online_service wifi_online_service.py")handle.wait()
正如上面提到的,我们这里介绍一个将目录打包为安装文件的工具程序。该工具程序名为Inno Setup,是一个免费的工具软件,网站主页如下图8-1所示。可以点击Download Inno Setup链接进行下载,详细信息可以登陆网站()进行了解。
![5df399d3f5fec6725757ef7c9a483edf.png](https://i-blog.csdnimg.cn/blog_migrate/07c2e396a3f7af23d3b6f4ebeb55ba3d.jpeg)
图15-1 Inno Setup主页
这里,我们要创建服务的安装程序,跟普通的程序安装有点区别。服务程序在安装完毕后需要安装服务,并启动服务。而普通程序安装完成中,不需要这些额外的操作,因此我们需要对Inno Setup生的iss文件做一些修改。完整的文件如下,大部分是Inno Setup自动生成的,我们这里只是修改[Run]和[UninstallRun]里面的内容,可以参考修改自己的。在[Run]里面增加的内容是安装完成后,我们的程序执行文件带着参数--startup auto install进行执行一次,然后再带着参数start执行一次,这也就意味着我们的服务被安装为电脑启动的时候自动执行,并且当前也启动了该服务。而[UninstallRun]里面添加的内容意味着,在卸载时先停止服务,再删除服务。; Script generated by the Inno Setup Script Wizard.; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!#define MyAppName "wifi_online_service"#define MyAppVersion "1.0"#define MyAppPublisher "My Company, Inc."#define MyAppURL "http://www.example.com/"#define MyAppExeName "wifi_online_service.exe"[Setup]; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)AppId={{13993919-2190-41BB-9CD1-2DC8A56671E2}AppName={#MyAppName}AppVersion={#MyAppVersion};AppVerName={#MyAppName} {#MyAppVersion}AppPublisher={#MyAppPublisher}AppPublisherURL={#MyAppURL}AppSupportURL={#MyAppURL}AppUpdatesURL={#MyAppURL}DefaultDirName={autopf}{#MyAppName}DefaultGroupName={#MyAppName}DisableProgramGroupPage=yes; Uncomment the following line to run in non administrative install mode (install for current user only.);PrivilegesRequired=lowestOutputDir=E:实现一个Windows上的服务OutputBaseFilename=wifi_online_service_setupCompression=lzmaSolidCompression=yesWizardStyle=modern[Languages]Name: "english"; MessagesFile: "compiler:Default.isl"[Files]Source: "E:实现一个Windows上的服务distwifi_online_servicewifi_online_service.exe"; DestDir: "{app}"; Flags: ignoreversionSource: "E:实现一个Windows上的服务distwifi_online_service*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; NOTE: Don't use "Flags: ignoreversion" on any shared system files[Icons]Name: "{group}{#MyAppName}"; Filename: "{app}{#MyAppExeName}"[Run]Filename: "{app}{#MyAppExeName}"; Parameters: "--startup auto install" ; Flags: runhiddenFilename: "{app}{#MyAppExeName}"; Parameters: "start" ; Flags: runhidden[UninstallRun]Filename: "{app}{#MyAppExeName}"; Parameters: "stop" ; Flags: runhiddenFilename: "{app}{#MyAppExeName}"; Parameters: "remove" ; Flags: runhidden
这里打包生成的安装文件是一个exe文件,我们双击执行安装时会出现如图8-2所示界面,可以看出是一个很常见的程序安装界面,跟其他程序安装没有什么区别,看起来更专业。
![9ede9bbe14f1151b9044450e2f54fcea.png](https://i-blog.csdnimg.cn/blog_migrate/214942d26834db0150a88c0b14a9dc4f.jpeg)
图15-2 服务程序安装界面
到这里,程序的打包安装就介绍完毕,具体选择哪种方式进行打包,可以根据具体情况而定。写一个自己的服务程序,进行安装,观察一下日志文件,看看具体发生了什么。
15.5 本章小结
本章介绍了如何用Python编写一个Windows服务程序,个人觉得是一个非常有用的功能。可以很容易的将一些需要长期运行的程序进行改写为服务进行安装,这样就能免去每次开机都需要去手动执行的的麻烦,并且用户也感知不到它的存在。
并且在本章也介绍了将Python程序打包为单个exe文件和目录方法及程序,另外也介绍了一个免费的Windows打包工具软件Inno Setup,可以方便的用于打包您的程序。该工具不局限于打包Python程序,它可以用于打包任何其他程序,是一个比较好用的工具软件。
欢迎关注,转发,收藏
Python实用案例编程入门:第一章 Python概述及为什么学Python
Python实用案例编程入门:第二章 字符串
Python实用案例编程入门:第五章 函数和类
Python实用案例编程入门:第四章 字典和文件
Python实用案例编程入门:第三章 列表和元组
Python实用案例编程入门:第七章 调式手段
Python实用案例编程入门:第六章 控制流语句
Python实用案例编程入门:第十章 用Python处理音频文件
Python实用案例编程入门:第九章 爬虫下载VOA每日广播英语MP3
Python实用案例编程入门:第十二章 测试线预订程序
Python实用案例编程入门:第十一章 做一个年会抽奖程序
Python实用案例编程入门:第十三章 自动收发电子邮件,远程控制
Python实用案例编程入门:第十四章 通过声音控制您的计算机
Python实用案例编程入门:第八章 如何自动连接WIFI