2022-01-18 在Django 4.x中实现Web方式执行系统命令和结果显示 (3)

在Django 4.0中实现Web方式执行系统命令和结果显示 (2)  里使用 Websocket 实现运行长时间的命令,测试 netstat 命令时,发现Channels 调用 CmdConsumer receive 的通道被异步打开的 subprocess.Popen 阻塞,即使 Popen 结束,从客户端发起Websocket close, 也会导致服务端抛出超时异常。

本文在(2)的基础上(项目名称不变),添加了多线程、忙碌标记、页面上添加了 Close Websocket 按钮。对比(2),代码变化,主要在 home/templates/home.html 和 home/consumers.py 。

1. 开发环境

    Windows 10 Home (20H2) or Ubuntu 18.04

    Python 3.8.1

    Pip 21.3.1

    Django: 4.0

    Windows下搭建开发环境,请参考Windows下搭建 Django 3.x 开发和运行环境

    Ubuntu下搭建开发环境,请参考Ubuntu下搭建 Django 3.x 开发和运行环境


2. 创建 Django 项目

    > django-admin startproject djangoWebsocketDemo


3. 添加 App

    > cd djangoWebsocketDemo

    > python manage.py startapp home

     生成的项目目录结构,参考 如何在Django中使用template和Bootstrap

        修改 djangoWebsocketDemo/settings.py

        ALLOWED_HOSTS = ['localhost', '192.168.0.5']

        ...

        INSTALLED_APPS = [

            ...

            'home',

        ]

        ...

        # Create static folder

        STATICFILES_DIRS = [

            BASE_DIR / 'static',

        ]


4. 静态资源和模板

    1)  静态资源

        从 https://jquery.com/ 下载 jQuery 包, 添加到 :

            static/js/jquery-1.12.2.min.js

        * static 等中间目录如果不存在,请新建它们,下同。

    2) 添加 home/templates/home.html

   

    Home Page

    {% load static %}

       

   

Home Page


   

        {% csrf_token %}


       

           

           

       

       

           

           

       

       

           

           

       

       

           

             

       

       

           

                         

       

   

 

   

 

   

   

   

 

   


5. 视图和路由

    1) 添加 home/utils.py

        import subprocess

        def openPipe(rsyncStr, shell=True, b_print=True):

            return subprocess.Popen(rsyncStr, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        def systemExecute(rsyncStr, shell=True, b_print=True):

            #print(rsyncStr)

            p = openPipe(rsyncStr, shell, b_print)

            out, err = p.communicate()

            return out.decode("gbk", "ignore"), err.decode("gbk", "ignore")

        def systemExecuteLines(rsyncStr, shell=True, b_print=True):

            #print(rsyncStr)

            p = openPipe(rsyncStr, shell, b_print)

            out, err = p.communicate()

            return out.decode("gbk", "ignore").splitlines(), err.decode("gbk", "ignore").splitlines()

        * 显示中文时需要 decode 设置为 gbk

    2) 修改 home/views.py

        from django.shortcuts import render

        # Create your views here.

        def home(request):

            return render(request, "home.html")

    3) 修改 djangoWebsocketDemo/urls.py

        from django.contrib import admin

        from django.urls import path

        from home import views

        urlpatterns = [

            path('', views.home, name='home'),

            path('admin/', admin.site.urls),

        ]


6. Django 自定义命令

    1) 添加 home/management/commands/DirCommand.py

        from django.core.management.base import BaseCommand

        from home.utils import systemExecute

        class Command(BaseCommand):

            help = "Run 'dir' command on Windows or Linux."

            def add_arguments(self, parser):

                parser.add_argument("path")

            def handle(self, *args, **options):

                data,err = systemExecute("dir " + options["path"])

                if err:

                    print(err)

                else:

                    print(data)

    2) 命令行方式运行 DirCommand

        > python manage.py DirCommand c:\          # On Windows

            Volume in drive C is WINDOWS

            Volume Serial Number is D46B-07AC

            Directory of c:\

            2021/12/28  10:57   

          Program Files

            2021/12/28  13:50   

          Program Files (x86)

            2021/12/20  12:15   

          Users

            2021/12/29  11:26   

          Virtual

            2022/01/05  18:18   

          Windows

            0 File(s)              0 bytes

            ...

        $ python manage.py DirCommand /    # On Ubuntu

            bin    dev  root    usr    etc    lib  mnt  var

            home  lib64    opt    sbin  sys  cdrom

            ...


7. Channels

    https://pypi.org/project/channels/

    1) 安装 Channels

        $ pip install channels      # pip 版本 21.3.1,低版本pip可能无法安装

    2) 修改 djangoWebsocketDemo/settings.py

        INSTALLED_APPS = [

        ...

        'channels',        # Add

        ]

        ...

        ASGI_APPLICATION = 'djangoWebsocketDemo.asgi.application'      # Add

        CHANNEL_LAYERS = {    # 频道后端,这里采用内存存储,默认是redis

            "default": {

                "BACKEND": "channels.layers.InMemoryChannelLayer"

            }

        }

    3) 添加 home/consumers.py

import json, _thread

from channels.generic.websocket import WebsocketConsumer

from home.utils import openPipe

class CmdConsumer(WebsocketConsumer):

    pipe_status = 0    # 0 - PIPE ready;  1 - PIPE busying; 2 - PIPE stopping;

    close_code = 0

    def threadFunc(self, cmdStr):

        p = openPipe(cmdStr)

        if p:

            self.pipe_status = 1

            for line in p.stdout:

                #print("threadFunc() -> p.stdout: self.pipe_status = " + str(self.pipe_status))

                if self.pipe_status == 2: 

                    break

                self.send(text_data=json.dumps({

                        "ret": "data",

                        "message": line.decode("gbk", "ignore").rstrip("\r\n")

                    }))

            if self.pipe_status != 2:

                err = p.stderr.read()

                if err:     

                    self.send(text_data=json.dumps({

                            "ret": "error",

                            "description": err.decode("gbk", "ignore").rstrip("\r\n")

                        }))

            #print("threadFunc() -> p.stdout: self.pipe_status = " + str(self.pipe_status) + ", close_code = " + str(self.close_code))

            if self.pipe_status == 2 and self.close_code == 0:

                self.send(text_data=json.dumps({

                    "ret": "finish",

                    "description": "Finish after shutdown PIPE"

                }))


            self.pipe_status = 0                   

        else:         

            self.send(text_data=json.dumps({

                    "ret": "error",

                    "description": "Unable to open PIPE"

                }))

    def connect(self):

        self.accept()

    def disconnect(self, close_code):

        print("disconnect(): close_code = " + str(close_code) + ", pipe_status = " + str(self.pipe_status))

        self.close_code = close_code

        if self.pipe_status == 1:

            self.pipe_status = 2

    def receive(self, text_data):

        print("receive(): text_data = ", text_data)       

        message = dict(json.loads(text_data))

        if message["operation"] == "command":

            #print("receive() -> command: pipe_status = " + str(self.pipe_status))

            if self.pipe_status == 0:

                execStr = ""

                if message['param']['cmd_type'] == "system_cmd_shell":

                    execStr = message['param']['cmd_str']

                elif message['param']['cmd_type'] == "django_cmd_shell":

                    execStr = "python manage.py " +  message['param']['cmd_str']


                if execStr != "":

                    try:

                        _thread.start_new_thread(self.threadFunc, (execStr, ))

                    except:

                        self.send(text_data=json.dumps({

                                "ret": "error",

                                "description": "Unable to start thread"

                            }))

                else:

                    self.send(text_data=json.dumps({

                            "ret": "error",

                            "description": "Invalid command type"

                        }))                               

            else:

                self.send(text_data=json.dumps({

                        "ret": "error",

                        "description": "PIPE is busying"

                    }))

        elif message["operation"] == "close":

            #print("receive() -> close: pipe_status = " + str(self.pipe_status))

            if self.pipe_status == 0:

                self.send(text_data=json.dumps({

                        "ret": "finish",

                        "description": "Finish directly"

                    }))             

            elif self.pipe_status == 1:

                self.pipe_status = 2

        else:

            self.send(text_data=json.dumps({

                    "ret": "error",

                    "description": "Invalid data format"

                }))

    4) 添加 home/routing.py

        from django.urls import path

        from home import consumers

        websocket_urlpatterns = [

            path("ws/command/exec/", consumers.CmdConsumer.as_asgi()),

        ]

    5) 修改 djangoWebsocketDemo/asgi.py

        import os

        from django.core.asgi import get_asgi_application

        from channels.routing import ProtocolTypeRouter, URLRouter

        from channels.auth import AuthMiddlewareStack

        from home import routing

        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoWebsocketDemo.settings')

        application = ProtocolTypeRouter({

            "http": get_asgi_application(),

            "websocket": AuthMiddlewareStack(

                URLRouter(

                    routing.websocket_urlpatterns

                )

            ),

        })


8. 运行

    > python manage.py runserver

        访问 http://localhost:8000/

    > python manage.py runserver 192.168.0.5:8080

        访问 http://192.168.0.5:8080/

你可能感兴趣的:(2022-01-18 在Django 4.x中实现Web方式执行系统命令和结果显示 (3))