本人遇到一个项目,需求是实现一个客户端界面,该客户端可读取摄像头、读卡器这些本地设备,同时也需要访问云服务器的公共服务,安装程序包尽量小,且安装简单。
针对这种需求,当然会有很多种解决方法,比如使用c#、VB、Java等开发语言,是可以实现的,但选择使用Python的原因如下:
确定开发语言后,项目的架构设计成:
本项目在实现上述本地服务端程序时,首先想到的是建立windows服务,此服务中实现端口监听和相应逻辑,但最后发现这种方式并不稳定;随后修改为使用python中自带的pythonw.exe特性进行启动服务监听端口,此方法还较稳定。
因此,本文分成2部分介绍,第1部分介绍windows服务中遇到的坑,第2部分介绍使用pythonw.exe的完整过程。在介绍过程中已经省去了与项目特性相关的内容。
主要的工具包就这些,如果其中还需其他依赖包,可再进行安装。
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、安装与配置
[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
_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服务的入口。
编译和运行过程中可能会存在如下报错:
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)
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)
无标题.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】命令
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
[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
小结:
本节主要介绍了使用django-windows-tools及pyinstaler打包过程中遇到的各种问题,在本文最后再贴出源码。
2.3.2、打包成单文件形式
打包生成单文件,可有2种方法:
打包完后,最终运行时如:
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 )
作者:tianxiawuzhe
链接:https://www.jianshu.com/p/a53b430b1410
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。