使用 Python 實作 Windows Service
import win32serviceutil
import win32service
import win32event
import os
import logging
import inspect
class PythonService(win32serviceutil.ServiceFramework):
# 服務名稱
_svc_name_ = "PythonService"
# 服務在 Windows 系統中顯示的名稱
_svc_display_name_ = "Python Service Test"
# 服務的描述
_svc_description_ = "This is a python service test code "
# PythonService 的 __init__ 函數執行完後,系統服務開始啟動
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
self.logger = self._getLogger()
self.run = True
def _getLogger(self):
logger = logging.getLogger('[PythonService]')
this_file = inspect.getfile(inspect.currentframe())
dirpath = os.path.abspath(os.path.dirname(this_file))
handler = logging.FileHandler(os.path.join(dirpath, "service.log"))
formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
# Windows 系統會自動呼叫 SvcDoRun 函式,這個函式的執行不可以結束,因為結束就代表服務停止。
# 所以當我們放自己的程式碼在 SvcDoRun 函式中執行的時候,必須確保該函式不退出,
# 如果退出或者該函式沒有正常執行就表示服務停止,Windows 系統會提示在狀態欄位中
def SvcDoRun(self):
# 把自己的程式碼放到這裡,就OK
# 等待服務被停止
# win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
import time
self.logger.info("service is run....")
while self.run:
self.logger.info("I am runing....")
time.sleep(2)
# 當停止服務的時候,系統會呼叫 SvcDoStop 函式,該函式通過設定標誌位等方式讓 SvcDoRun 函式退出,就是正常的停止服務。
# 例子中是通過 event 事件讓 SvcDoRun 函式停止等待,從而退出該函式,從而使服務停止。
# 注意:系統關機時不會呼叫SvcDoStop函式,所以這種服務是可以設定為開機自啟的。
def SvcStop(self):
self.logger.info("service is stop....")
# 先告訴 SCM 停止這個服務
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# 設定事件
win32event.SetEvent(self.hWaitStop)
self.run = False
if __name__=='__main__':
# 括號裡引數可以改成其他名字,但是必須與 class 類別名稱一致
win32serviceutil.HandleCommandLine(PythonService)
由於設定為 Windows Service ,因此使用
import os
os.getcwd()
會抓到 PythonService.exe 的路徑,因此不能使用 os.getcwd() 來獲取執行檔的目錄。
環境變數的設定也要修改,讓 PythonService.exe 優先被找到。例如,在 anaconda3 內的 envs 建立 Work37Service,那環境變數範例如下:
C:\Users\{username}\anaconda3\envs\Work37WindowsService\;
C:\Users\{username}\anaconda3\envs\Work37WindowsService\Lib\site-packages\pywin32_system32\;
C:\Users\{username}\anaconda3\envs\Work37WindowsService\Lib\site-packages\win32\;
C:\Users\{username}\anaconda3\envs\Work37WindowsService\Scripts;
C:\Users\{username}\anaconda3\envs\Work37WindowsService\Library\bin;
C:\Users\{username}\anaconda3\envs\Work37WindowsService\Library\mingw-w64\bin;
要加入系統環境變數 System Path 的開頭,而不是 User Path。加入 User Path 不會起作用。
import os
import sys
import win32serviceutil
import win32service
import win32event
import logging
import inspect
class PythonService(win32serviceutil.ServiceFramework):
_svc_name_ = "PythonService"
_svc_display_name_ = "Python Service Test"
_svc_description_ = "This is a python service test code "
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
self.logger = self._getLogger()
self.run = True
def _getLogger(self):
logger = logging.getLogger('[PythonService]')
# this_file 會存有包含路徑及檔案的完整路徑
this_file = inspect.getfile(inspect.currentframe())
# dirpath 會存有完整的路徑
dirpath = os.path.abspath(os.path.dirname(this_file))
handler = logging.FileHandler(os.path.join(dirpath, "service.log"))
formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
def SvcDoRun(self):
import time
self.logger.info("service is run....")
while self.run:
self.logger.info("I am runing....")
time.sleep(2)
def SvcStop(self):
self.logger.info("service is stop....")
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
win32event.SetEvent(self.hWaitStop)
self.run = False
def PostServiceUpdate(*args):
import win32api, win32con, win32profile, pywintypes
from contextlib import closing
env_reg_key = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"
hkey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, env_reg_key, 0, win32con.KEY_ALL_ACCESS)
with closing(hkey):
system_path = win32api.RegQueryValueEx(hkey, 'PATH')[0]
# PATH may contain %SYSTEM_ROOT% or other env variables that must be expanded
# ExpandEnvironmentStringsForUser(None) only expands System variables
# PATH 可能包含 %SYSTEM_ROOT% 或其他必須擴展的環境變量
# ExpandEnvironmentStringsForUser(None)僅擴展系統變量
system_path = win32profile.ExpandEnvironmentStringsForUser(None, system_path)
system_path_list = system_path.split(os.pathsep)
core_dll_file = win32api.GetModuleFileName(sys.dllhandle)
core_dll_name = os.path.basename(core_dll_file)
for search_path_dir in system_path_list:
try:
dll_path = win32api.SearchPath(search_path_dir, core_dll_name)[0]
print(f"System python DLL: {dll_path}")
break
except pywintypes.error as ex:
if ex.args[1] != 'SearchPath': raise
continue
else:
print("*** WARNING ***")
print(f"Your current Python DLL ({core_dll_name}) is not in your SYSTEM PATH")
print("The service is likely to not launch correctly.")
from win32serviceutil import LocatePythonServiceExe
pythonservice_exe = LocatePythonServiceExe()
pywintypes_dll_file = pywintypes.__spec__.origin
pythonservice_path = os.path.dirname(pythonservice_exe)
pywintypes_dll_name = os.path.basename(pywintypes_dll_file)
try:
return win32api.SearchPath(pythonservice_path, pywintypes_dll_name)[0]
# 當發現 C:\Users\{username}\anaconda3\envs\Work37WindowsService\Lib\site-packages\win32 內沒有 pywintypes37.dll,
# 就會發出警告,要求將 pywintypes37.dll 複製與 pythonservice.exe 相同目錄下
except pywintypes.error as ex:
if ex.args[1] != 'SearchPath': raise
print("*** WARNING ***")
print(f"{pywintypes_dll_name} is not is the same directory as pythonservice.exe")
print(f'Copy "{pywintypes_dll_file}" to "{pythonservice_path}"')
print("The service is likely to not launch correctly.")
# customOptionHandler will only run after service install/update
if __name__=='__main__':
win32serviceutil.HandleCommandLine(PythonService, customOptionHandler=PostServiceUpdate)
服務操作命令¶
下面是對上述服務操作的基本命令:
- 安裝服務
python PythonService.py install - 讓服務自動啟動
python PythonService.py --startup auto install
P.S. 不過有時候服務並未啟動時,還得額外下 python PythonService.py start 讓服務啟動。因此還是要檢查服務是否有啟動。 - 啟動服務
python PythonService.py start - 重啟服務
python PythonService.py restart - 停止服務
python PythonService.py stop - 刪除/解除安裝服務
python PythonService.py remove - 偵錯服務
python PythonService.py debug
P.S. 這樣才會讓你的 print() 函數顯示出來,否則甚麼也看不到。 - 更新服務
python PythonService.py update
對於原始程式碼修改注意事項¶
Q1. 原來使用 os.getcwd() 的路徑變成指向 PythonService.exe 的路徑,該如何快速解決?
A1. 使用 Replace in File ,把所有 os.getcwd() 全部指向絕對路徑,這是比較快的做法。
Q2. 原來的模組無法呼叫到,該如何解決?
A2. 加入 __init__.py 把模組改為套件,使用套件的模式來引用。
Q3. 如何 Debug 自己寫的 Python Windows Service。
A3. 使用 python PythonService.py{應用程式名稱} debug,這樣就可以看到執行的過程。
Q4. 無法啟動服務原始碼,該如何解決?
A4. 將整合開發工具以系統管理員的權限開啟,就可以編輯服務原始碼。
Q5. 如何透過命令列刪除服務?
A5. sc delete PythonService
Q6. 如何透過命令列停止服務?
A6. net stop PythonService
Q7. 如何徹底移除服務的登錄?
A7. Register Edit 開啟 Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services ,找到服務機鍵,刪除掉。
Q8. 如何讀取主程式根目錄位置?
A8.
- 寫入根目錄位置到根目錄及 PythonService.exe 所在目錄。
from win32serviceutil import LocatePythonServiceExe pythonservice_exe = LocatePythonServiceExe() pythonservice_path = os.path.dirname(pythonservice_exe) this_file = inspect.getfile(inspect.currentframe()) dirpath = os.path.abspath(os.path.dirname(this_file)) fRootPath = open("./RootPath.txt", "w+", encoding="UTF-8") fRootPath.write(dirpath.replace("\\","/")+"\n") fRootPath.close() fRootPath = open(pythonservice_path.replace("\\","/")+"/RootPath.txt", "w+", encoding="UTF-8") fRootPath.write(dirpath.replace("\\","/")+"\n") fRootPath.close()
- 讀取根目錄位置。
with open("RootPath.txt", "r", encoding="UTF-8") as fRootPath: RootPath = fRootPath.read()
Comments
Post a Comment