Python 軟件熱更新

Python 軟件熱更新

本篇文章涉及技術知識以下:python

Redis
threading 多線程
PyQt5git

importlib 熱更新github

場景

我們在平時運行一些長時間都會一直運行的軟件(如:某些雲同步軟件)的時候,某些功能由於考慮的狀況可能不充分,致使體驗不夠好的時候,不少人都會忽視這個問題,除非這個問題影響到他正常使用了。可是也有部分用戶會在軟件的反饋框裏面將問題反饋給開發者,順帶將錯誤日誌也一併提交給開發者。而後過了一天或者半天,你再運行那部分功能的時候,發現問題已經解決了。但是,咱們都沒有更新軟件呀,甚至連軟件都沒有重啓,難道前面遇到的那個狀況真的是由於本身太幸運踩中bug了嗎?
其實,咱們以前遇到的問題,可能的確就是一個bug,可是在反饋問題給開發者後,開發者快速定位問題所在後,經過熱更新將問題解決了。至關於咱們使用的軟件自動fix了一些bug,更新了一次版本。
那麼,今天我們聊一下熱更新這個東西怎麼樣?咱們也隨意作個小demo看看這個有意思的功能是怎麼作到的。redis

什麼是熱更新

熱更新就是能夠在進程不重啓的狀況下,讓其從新加載修改後的程序代碼,且能按照預期正確執行。在實際開發中,熱更新的最主要用途有,數據庫

  • 能夠提高開發效率,讓改動後的代碼效果馬上實現,避免頻繁重啓
  • 對於bug修復來講,在CS模式下,若是不是大的bug而是小bug的修復就不用發佈比較大的補丁和更新文件了,直接使用服務器修正問題後,通知客戶端從新加載修正後代碼便可。

Python的代碼是經過module進行組織的,因此,對某些功能的熱更新就是能夠經過對module更新就能夠了。
在Python中,若是從新import 一個已經被import的模塊時,並不會從新執行import新的模塊。因此,在這個時候,咱們但願能夠從新加載模塊的時候,就須要對模塊進行刪除後,再從新import進來。
而在sys.modules保存了已經加載過的模塊。瀏覽器

 

 

爲了方便看到展現,我就沿用上次客戶端的界面,進行簡單修改後,展現給你們看,熱更新的效果。服務器

 

 

左邊的按鈕是運行模塊加載進來的函數,右邊的按鈕是手動點一下熱更新。便於本地手動調試熱更新。在後面實現的「發佈訂閱」狀況中,服務端發佈更新消息後,不用手動點 熱更新 就能夠對軟件進行自動更新了。微信

簡單實現一個demo,引用myfunction這個模塊,運行裏面的某個函數一兩次後,修改那個被運行的函數實現,而後對myfunction這個模塊進行熱更新,看看效果怎麼樣?多線程

 

 

在熱更新前,隨機產生的數字在原函數裏面,版本號爲0.0.1,是能夠比較明顯看出 兩個數 是運行 「相加」 操做的。
點擊了熱更新Button後,軟件並未重啓,在更新後,能夠 看到功能版本號發生了改變,變成了0.1.1,說明已是熱更新完成了的。再點擊運行功能,能夠看到結果已經變成了 兩個數字 進行了 「相減‘ 操做。app

完成了本地測試熱更新成功後,就着手實現CS模式下的「發佈訂閱」消息通知功能,利用服務器對客戶端推送一個更新指令,客戶端就會自動更新模塊。
用過Redis的同窗應該都知道Redis自己就自帶了「發佈訂閱」功能,藉助它,能夠很方便的實現出遠程推送消息的需求,咱們甚至能夠用這個功能實現一個簡單的聊天室軟件哦。
在Redis服務端中,建立一個 update頻道:

SUBSCRIBE update

而後在Python中導入Redis模塊後,連接到遠程Redis數據庫後,訂閱咱們的update頻道,再啓動一個新的線程去監聽update頻道的消息。
由於若是直接在代碼裏面用單線程去監聽消息的話,會形成線程阻塞在監聽消息哪裏,致使界面刷新不出來。因此,咱們只要導入threading庫,再把監聽消息作成一個函數,放到thread中去運行就能夠了。由此避免線程阻塞問題。

接着我在本地修改一下myfunction模塊後,就到Redis服務的終端中,發佈一個消息,reload。
這個時候,軟件就會收到reload消息,對剛纔被我修改後的模塊進行熱更新,即刪掉源模塊,再從新導入一次。
在這裏我就不寫一次從服務器中下載新的模塊文件的代碼了,假設我剛纔修改後的那個文件就從服務器下載下來的。同窗們能夠藉助前面兩篇寫軟件更新服務的文章來本身實現一個文件下載更新的代碼。很簡單的,只要你願意寫。

在Redis中發佈重載的消息後,訂閱了這個頻道的客戶端,將會接收到更新信息,好比我這個客戶端,將會對模塊從新加載進行熱更新了。
在這裏我給你們隨意扯一下「灰度測試」吧,這個灰度測試就是軟件即將要更新某個功能,可是可能這個功能還不夠穩定,不能向所有用戶推送新的功能。因此,這個時候,就須要對部分用戶更新這個功能,經過這部分用戶使用狀況來決定灰度測試的範圍,好比將5%的範圍擴展到10%這樣。
至於怎麼篩選出那些用戶爲測試用戶呢?其中有個辦法是能夠用個哈希函數對用戶某個值,如用戶名,進行處理,符合的就推送。固然還有不少不少的策略,實現執行起來的時候,也不會像我說的那麼簡單,感興趣的同窗能夠自行查閱資料。

接下來,咱們來測試一下發布更新功能的消息後,有沒正常熱更新功能。

 

 

 

 

在這裏要提醒一下,若是你在熱更新前導入的模塊生成了一個對象x,這個時候,你熱更新了,而後又生成一個對象y。這個時候,你會發現,x指向的仍舊是舊的那個類,而y則指向了新的類。這個時候,能夠經過修改x的__class__屬性來對 x 的類進行強制修改,能夠這樣寫:

x.class == y.你的類

可是即便你是這樣寫,你x裏面的數據仍舊不會發生改變的哦。咱們只更改了代碼的執行邏輯。
代碼放這裏了:

    python    61行

# -*- coding: utf-8 -*-
# @Time    : 4/12/2019 14:02
# @Author  : MARX·CBR
# @File    : __init__.py.py
import threading
import sys
from PyQt5 import QtWidgets
from updateServer.HotUpdate import myfunction
import redis
import random
import importlib
from updateServer.HotUpdate.HotFixSample import Ui_MainWindow


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setupUi(self)
        self.fun = importlib.import_module("myfunction")
        self.pushButton.clicked.connect(self.runFunction)
        self.pushButton_2.clicked.connect(self.hotfix)
        self.ip = "47.xxx.xxx.xx"
        self.redisport = 2017
        self.redis_manager = redis.StrictRedis(self.ip, port=self.redisport)
        # self.textBrowser.append(str(sys.modules))
        print(sys.modules)
        self.tunnel = self.redis_manager.pubsub()
        self.tunnel.subscribe(["update"])
        self.threads = []
        self.t1 = threading.Thread(target=self.autoReload, )
        self.threads.append(self.t1)
        self.threads[0].setDaemon(True)
        self.threads[0].start()

    def autoReload(self):

        for k in self.tunnel.listen():
            if k.get('data') == b'reload':
                self.hotfix()

    def runFunction(self):
        version = self.fun.AllFunction().version
        self.textBrowser.append("功能運行,當前版本爲:" + version)
        for i in range(4):
            x = random.randint(-454, 994)
            y = random.randint(-245, 437)
            self.textBrowser.append(str(x) + "\tfunction version {}\t".format(version) + str(y) + " = " + str(
                self.fun.AllFunction().second(x, y)))
        # self.textBrowser.append(self.fun.AllFunction().first())

    def hotfix(self):
        del sys.modules["myfunction"]
        self.fun = importlib.import_module('myfunction')
        self.textBrowser.append("熱更新完畢")


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())
 

總結


 在本篇文章中,咱們講到了Python的熱更新的一些簡單用法,以及一些值得注意的坑。順手應用了Redis的「發佈訂閱」功能來通知客戶端更新功能,扯了一下「灰度測試」。本篇文章代碼已同步上傳到我GitHub中,歡迎你們fork使用,順手給個star就更好了,
項目連接:https://github.com/97CBR/SoftwareUpdateServer

    其實,有心思的小夥伴確定會有更多的想法。好比:咱們既然能夠動態從新加載一個類來fix bug,也確定能夠動態添加咱們要的功能啦。這意味着,咱們能夠編寫出一個軟件,具備插件功能的軟件。在主體軟件上面,運行插件來擴展更加多的功能,和Chrome這樣的瀏覽器同樣,安裝插件什麼的。

本文對你有沒幫助呀,喜歡的話,記得留言、點贊、轉發喲。謝謝各位!

掃碼關注微信公衆號

相關文章
相關標籤/搜索