• Python3.6+pyinstaller+Django


    方案(一)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:helloworldhellosettings.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:logsservice.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:logsservice.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>disthellohello.exe install
    E:helloworld>disthellohello.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-packageswin32libwin32serviceutil.py", line 835, in SvcRun
      File "site-packagesdjango_windows_toolsservice.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-packagesdjango_windows_toolsservice.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-packagesdjango_windows_toolsservice.py", line 156, in start_django_command
      File "site-packagesdjangocoremanagement\__init__.py", line 364, in execute_from_command_line
      File "site-packagesdjangocoremanagement\__init__.py", line 356, in execute
      File "site-packagesdjangocoremanagementase.py", line 277, in run_from_argv
      File "site-packagesdjangocoremanagementase.py", line 58, in parse_args
      File "argparse.py", line 1733, in parse_args
      File "site-packagesdjangocoremanagementase.py", line 62, in error
      File "argparse.py", line 2389, in error
      File "argparse.py", line 2376, in exit
    SystemExit: 2
    
      • d:logsservice.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>disthello.exe install
    E:helloworld>disthello.exe start
    

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

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

    2.4、其他可能遇到的问题

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

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

    2.4.2、PermissionError: [WinError 5] 拒绝访问:'E:helloworlddisthelloservicemanager.pyd'

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

    三、完整源码

    3.1、service.ini

    [services]
    # Services to be run on all machines
    run=runserver
    clean=APPLOGSservice.log
    
    [BEATSERVER]
    # There should be only one machine with the celerybeat service
    run=celeryd celerybeat
    clean=APPLOGScelerybeat.pid;APPLOGSeat.log;APPLOGScelery.log
    
    [celeryd]
    command=celeryd
    parameters=-f APPLOGScelery.log -l info
    
    [celerybeat]
    command=celerybeat
    parameters=-f APPLOGSeat.log -l info --pidfile=APPLOGScelerybeat.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=APPLOGSservice.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 )

    转载:https://www.jianshu.com/p/a53b430b1410

  • 相关阅读:
    Excel编程的基本概念
    Excel中的基本概念
    How to create Excel file in C#
    学生管理系统----当然封装类型
    iphone6 plus有什么办法
    买面包和IoC
    拆除vs发展c++程序开发过程中产生的.ipch和.sdf文件的方法
    Socket编程实践(4) --更复杂的过程server
    BestCoder Round #16
    流动python
  • 原文地址:https://www.cnblogs.com/mxhmxh/p/9367666.html
Copyright © 2020-2023  润新知