python 管理windows客户端_pyinstaller+python3.6搭建windows客户端的服务(一)

背景

本人遇到一个项目,需求是实现一个客户端界面,该客户端可读取摄像头、读卡器这些本地设备,同时也需要访问云服务器的公共服务,安装程序包尽量小,且安装简单。

针对这种需求,当然会有很多种解决方法,比如使用c#、VB、Java等开发语言,是可以实现的,但选择使用Python的原因如下:

本项目里成员对上述语言不喜欢或使用不太熟,而对Python相对比较熟;

上述语言在客户安装时可能会比较麻烦,比如Java需要安jdk或jre、C#需要安装.net(当然对于不需要安装基础环境的开发语言,项目成员肯定是不会的),加进来安装包将会比较大;

Python可以开发windows客户端,若使用pyinstaller打包,则安装包在压缩后不太大,可接受;

项目成员以前未开发过windows这种客户端,也想尝试下;

确定开发语言后,项目的架构设计成:

使用python+django服务器搭建一个本地服务器,负责读取本地摄像头、读卡器,负责与云服务交互;

客户直接在浏览器中访问本地url即可使用服务

使用pyinstaller将python工程打包成exe,然后使用Inno Setup Compiler进一步打包成安装压缩程序。

本项目在实现上述本地服务端程序时,首先想到的是建立windows服务,此服务中实现端口监听和相应逻辑,但最后发现这种方式并不稳定;随后修改为使用python中自带的pythonw.exe特性进行启动服务监听端口,此方法还较稳定。

因此,本文分成2部分介绍,第1部分介绍windows服务中遇到的坑,第2部分介绍使用pythonw.exe的完整过程。在介绍过程中已经省去了与项目特性相关的内容。

方案(一)Python3.6+pyinstaller+windows服务

一、Python3.6(64位)环境清单

Django==1.11.7

django-windows-tools==0.2

PyInstaller==3.3

主要的工具包就这些,如果其中还需其他依赖包,可再进行安装。

二、创建Helloworld

2.1、创建django工程示例

E:\>django-admin startproject helloworld

E:\helloworld>python manage.py startapp hello

若还有数据库,请按django正常流程进行数据库操作,更多详细操作可参见自强学堂的Django 基本命令。

然后就是尝试启动,看是否启动成功。

E:\helloworld>python manage.py runserver 8800

在浏览器中访问http://localhost:8800/,若可以显示django的服务接口清单,则表示服务启动正常,若不正常,请参考django的文档。

当然,还可以查看端口是否正常启动【netstat –ano | find “8800”】,如果有监听,则应该不会有问题。

2.2、添加windows服务功能

2.2.1、安装与配置

【pip install django-windows-tools】,可参见官方文档

添加django配置

在【E:\helloworld\hello\settings.py】文件中,向变量INSTALLED_APPS中添加'django_windows_tools'

生成项目的service.*文件

【E:\helloworld>python manage.py winservice_install】,此处可能与官方文档不一样,官方应该是错的。此时会自动生成service.py和service.ini两个文件

配置service.ini文件

官网有详细说明,大体为services是调用服务的入口,其中run的值为下文中设置的节点名称,如果run指令中含有多个节点命令,那么就会起多个线程来执行。本例中只会用到services、runserver、log这3个节点。其余节点可视情况配置。例如:

[services]

# Services to be run on all machines

run=runserver ## 表示会使用到下文的[runserver]中的命令,此名称可随意定

clean=d:\logs\service.log ## 要定期清理的日志路径,需与[log]中filename对应,否则在停止服务时会报找不到此日志文件

[runserver]

# Runs the debug server and listen on port 8000

# This one is just an example to show that any manage command can be used

command=runserver ## django的runserver命令

parameters=--noreload --insecure 0.0.0.0:8800 ## django的启动参数

[log]

filename=d:\logs\service.log

level=INFO

修改service.py文件:

为使服务更人性化,可在service.py文件的_svc_display_name_变量后面添加服务的描述【_svc_description_】:

_svc_display_name_ = "HelloWorldService"

_svc_description_ = "HelloWorldService" ## 建议处

_config_filename = "service.ini"

2.2.2、手工添加和启动服务

E:\helloworld>python service.py install

Installing service HelloWroldService

Service installed

E:\helloworld>python service.py start

Starting service HelloWroldService

此时,浏览器访问url能正常显示服务接口清单,且端口都正常,表示服务启动正常,windows服务搭建进行到一半了。一般此步骤之前不会存在问题。

2.3、打包工程Pyinstaller

如何下载安装就不讲解了,官网非常详细。

2.3.1、先打包成文件夹形式(方便排查问题)

E:\helloworld>pyinstaller -n hello -y --add-data "service.ini;." service.py

E:\helloworld>dist\hello\hello.exe install

E:\helloworld>dist\hello\hello.exe start

上述会在当前目录下生成dist文件夹,下面是生成的最终打包的文件。

-add-data表示将service.ini手工添加到打包程序中,若不添加,在运行时会提示找不到此文件。

-y表示自动覆盖上次的打包程序。

最后的参数service.py为windows服务的入口。

编译和运行过程中可能会存在如下报错:

问题1:缺少win32timezone包

直接在service.py中添加【import win32timezone】,pyinstaller就能自己找到加载包了

问题2:服务install正常,但start异常:

报【Error starting service: 服务没有及时响应启动或控制请求。】

此报错可根据网上的解决方法(具体原理本人也不清楚,能解决问题就可以了):

import win32serviceutil

import traceback

import servicemanager

import winerror

......

if __name__ == "__main__":

if len(sys.argv) > 1 and sys.argv[1] == 'test':

test_commands(base_path)

else:

if len(sys.argv) == 1:

try:evtsrc_dll = os.path.abspath(servicemanager.__file__)

servicemanager.PrepareToHostSingle(Service)

servicemanager.Initialize('HelloService', evtsrc_dll)

servicemanager.StartServiceCtrlDispatcher()

except Exception as exp:

print('ERROR : %s, Detail : %s' % (exp, traceback.format_exc()))

if exp.args[0] == winerror.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:

win32serviceutil.usage()

else:

win32serviceutil.HandleCommandLine(Service)

问题3:启动时不会报错,但端口是没有监听,windows事件查看器中会显示报错:

The instance's SvcRun() method failed

Traceback (most recent call last):

File "site-packages\win32\lib\win32serviceutil.py", line 835, in SvcRun

File "site-packages\django_windows_tools\service.py", line 269, in SvcDoRun

FileNotFoundError: [WinError 2] 'C:\\Windows\\system32\\service.ini'

即找不到service.ini文件,虽然我们打包时添加了此文件,但此exe文件在实际运行时并没有查找找到当前目录的文件。原因与pyinstaller运行机制有关,详见文档,需修改service.py文件import后中配置文件路径:

base_path = os.path.dirname(os.path.abspath(__file__))

if getattr(sys, 'frozen', False):

base_path = sys._MEIPASS ## 表示在实际生产运行时让程序去此目录查找

if not base_path in sys.path:

sys.path.append(base_path)

问题4:启动时不会报错,windows事件查看器中会显示报错(即问题3已经修改),日志文件中也无报错记录,但端口是没有监听,windows事件查看器中现象为:在打印出'starting'后就停止打印其他信息,未打印出'Starting command'。

正常日志应显示'Starting command D:\...\site-pack..\service.py runserver --noreload --insecure 0.0.0.0:8800'。

分析过程:

手工在site-packages\django_windows_tools\service.py文件中添加日志分析,发现在调用链【self.start()】-【start_commands()】-【spawn_command()】-【start_django_command()】中,最后那次调用未生效,即spawn_command中生成Process多线程处理时未生效;经google后,找到了PyInstaller-built Windows EXE fails with multiprocessing ,里面提到

无标题.png也就是说在windows平台中需要添加freeze_support()函数。

解决方法:

if __name__ == "__main__":

multiprocessing.freeze_support()

问题5:线程正常启动,事件查看器中显示报错:

pyinstall少包.png

说明我们在pyinstaller打包时少打了包。经过多次这种尝试,最终找出了所有django所需的包,需修改hello.spec文件,在【hiddenimports】数组中添加如下包:

hiddenimports=['django.contrib.admin.apps', 'django.contrib.auth.apps', 'django.contrib.contenttypes.apps', 'django.contrib.messages.apps', 'django.contrib.staticfiles.apps', 'django.contrib.sessions.models', 'django.contrib.sessions.apps', 'django.contrib.messages.middleware', 'django.contrib.auth.middleware', 'django.contrib.sessions.middleware', 'django.contrib.sessions.serializers']

注意,此时应执行:【pyinstaller -y hello.spec】命令

问题6:Django参数命令不正确。

事件查看器显示:

Exception occured : Traceback (most recent call last):

File "site-packages\django_windows_tools\service.py", line 156, in start_django_command

File "site-packages\django\core\management\__init__.py", line 364, in execute_from_command_line

File "site-packages\django\core\management\__init__.py", line 356, in execute

File "site-packages\django\core\management\base.py", line 277, in run_from_argv

File "site-packages\django\core\management\base.py", line 58, in parse_args

File "argparse.py", line 1733, in parse_args

File "site-packages\django\core\management\base.py", line 62, in error

File "argparse.py", line 2389, in error

File "argparse.py", line 2376, in exit

SystemExit: 2

d:\logs\service.log显示:

[INFO/Process-1] Starting command : service.py runserver --noreload --insecure 0.0.0.0:8800

[INFO/Process-1] usage: service.py runserver [-h] [--version] [-v {0,1,2,3}]

[--settings SETTINGS] [--pythonpath PYTHONPATH]

[--traceback] [--no-color] [--ipv6]

[--nothreading] [--noreload]

[addrport]

[INFO/Process-1] service.py runserver: error: unrecognized arguments: --insecure

即insecure参数不对,那就去掉吧。把service.ini的runserver节点下修改成【parameters=--noreload 0.0.0.0:8800】

小结:

本节主要介绍了使用django-windows-tools及pyinstaler打包过程中遇到的各种问题,在本文最后再贴出源码。

2.3.2、打包成单文件形式

打包生成单文件,可有2种方法:

通过命令行倒推:

先备份hello.spec文件,再运行命令【pyinstaller -n hello -y --add-data "service.ini;." –F service.py】添加-F参数,表示打包单文件;再打包生成新的hello.spec;然后把上述少的程序包添加到hiddenimports数组里;最后再进行打包。

直接修改hello.spec文件:

去掉【exclude_binaries=True,】,并添加【a.binaries, a.zipfiles, a.datas,runtime_tmpdir=None,】4行配置,再进行打包。

打包完后,最终运行时如:

E:\helloworld>dist\hello.exe install

E:\helloworld>dist\hello.exe start

比文件夹形式的,中间会少一层目录。

注意:运行单文件会比目录形式的时间长一点,它会首先解压至临时目录中,再运行,如果中间没有报错,就会立刻删除临时文件。因此,若项目中存在一些要时刻访问的配置文件,则需新建其他目录进行额外的管理。

2.4、其他可能遇到的问题

2.4.1、Error installing service: 指定的服务已标记为删除。 (1072)

一般为【服务】或【事件查看器】未关闭,关闭了即可解决,如果关闭了还出问题,可能就需要重启电脑,把与这个服务关联的进程给清理掉。

2.4.2、PermissionError: [WinError 5] 拒绝访问:'E:\helloworld\dist\hello\servicemanager.pyd'

一般为【事件查看器】未关闭,关闭了即可解决,如果关闭了还出问题,可能就需要重启电脑,把与这个服务关联的进程给清理掉。

三、完整源码

3.1、service.ini

[services]

# Services to be run on all machines

run=runserver

clean=APPLOGS\service.log

[BEATSERVER]

# There should be only one machine with the celerybeat service

run=celeryd celerybeat

clean=APPLOGS\celerybeat.pid;APPLOGS\beat.log;APPLOGS\celery.log

[celeryd]

command=celeryd

parameters=-f APPLOGS\celery.log -l info

[celerybeat]

command=celerybeat

parameters=-f APPLOGS\beat.log -l info --pidfile=APPLOGS\celerybeat.pid

[runserver]

# Runs the debug server and listen on port 8000

# This one is just an example to show that any manage command can be used

command=runserver

parameters=--noreload 0.0.0.0:18800

[log]

filename=APPLOGS\service.log

level=INFO

3.2、service.py

#!/usr/bin/env python

import os

import os.path

import sys

import win32serviceutil

import win32timezone

import traceback

import servicemanager

import winerror

import multiprocessing

import re

# This is my base path

base_path = os.path.dirname(os.path.abspath(__file__))

if getattr(sys, 'frozen', False):

base_path = sys._MEIPASS

if not base_path in sys.path:

sys.path.append(base_path)

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hello.settings")

from django_windows_tools.service import DjangoService,test_commands, error

class HelloService(DjangoService):

_base_path = base_path

_svc_name_ = "HelloService"

_svc_display_name_ = "HelloService"

_svc_description_ = "XXXXXXX"

_config_filename = "service.ini"

def __init__(self, args):

self.__replace_log_path()

DjangoService.__init__(self, args)

## 将service.ini手工移动到系统目录下的LOTS目录中

def __replace_log_path(self):

try:

file_dir = os.getenv('SYSTEMROOT') + '\\..\\LOTS'

if not os.path.exists(file_dir):

os.makedirs(file_dir)

old_file = os.path.join(HelloService._base_path, HelloService._config_filename)

new_file = old_file + '.run'

with open(old_file, 'r') as f:

old = f.read()

new = re.sub("APPLOGS", file_dir, old)

with open(new_file, 'w') as f:

f.write(new)

HelloService._config_filename = new_file

# win32file.CopyFile(new_file, old_file, 0)

except Exception as exp:

err = 'ERROR : %s, Detail : %s' % (exp, traceback.format_exc())

error(err)

if __name__ == "__main__":

multiprocessing.freeze_support()

argv_len = len(sys.argv)

if argv_len > 1 and sys.argv[1] == 'test':

test_commands(base_path)

else:

if argv_len == 1:

try:

evtsrc_dll = os.path.abspath(servicemanager.__file__)

servicemanager.PrepareToHostSingle(HelloService)

servicemanager.Initialize('HelloService', evtsrc_dll)

servicemanager.StartServiceCtrlDispatcher()

except Exception as exp:

print('ERROR : %s, Detail : %s' % (exp, traceback.format_exc()))

if exp.args[0] == winerror.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:

win32serviceutil.usage()

else:

win32serviceutil.HandleCommandLine(HelloService)

3.3、hello.spec——文件夹形式

# -*- mode: python -*-

block_cipher = None

a = Analysis(['service.py'],

pathex=['E:\\hello'],

binaries=[],

datas=[('service.ini', '.')],

hiddenimports=['django.contrib.admin.apps', 'django.contrib.auth.apps', 'django.contrib.contenttypes.apps', 'django.contrib.messages.apps', 'django.contrib.staticfiles.apps', 'django.contrib.sessions.models', 'django.contrib.sessions.apps', 'django.contrib.messages.middleware', 'django.contrib.auth.middleware', 'django.contrib.sessions.middleware', 'django.contrib.sessions.serializers'],

hookspath=[],

runtime_hooks=[],

excludes=[],

win_no_prefer_redirects=False,

win_private_assemblies=False,

cipher=block_cipher)

pyz = PYZ(a.pure, a.zipped_data,

cipher=block_cipher)

exe = EXE(pyz,

a.scripts,

a.binaries,

a.zipfiles,

a.datas,

exclude_binaries=True,

name='hello',

debug=False,

strip=False,

upx=True,

console=True )

coll = COLLECT(exe,

a.binaries,

a.zipfiles,

a.datas,

strip=False,

upx=True,

name='hello')

3.4、hello.spec——单文件形式

# -*- mode: python -*-

block_cipher = None

a = Analysis(['service.py'],

pathex=['E:\\hello'],

binaries=[],

datas=[('service.ini', '.')],

hiddenimports=['django.contrib.admin.apps', 'django.contrib.auth.apps', 'django.contrib.contenttypes.apps', 'django.contrib.messages.apps', 'django.contrib.staticfiles.apps', 'django.contrib.sessions.models', 'django.contrib.sessions.apps', 'django.contrib.messages.middleware', 'django.contrib.auth.middleware', 'django.contrib.sessions.middleware', 'django.contrib.sessions.serializers'],

hookspath=[],

runtime_hooks=[],

excludes=[],

win_no_prefer_redirects=False,

win_private_assemblies=False,

cipher=block_cipher)

pyz = PYZ(a.pure, a.zipped_data,

cipher=block_cipher)

exe = EXE(pyz,

a.scripts,

a.binaries,

a.zipfiles,

a.datas,

name='hello',

debug=False,

strip=False,

upx=True,

runtime_tmpdir=None,

console=True )

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值