#coding: utf-8
# +-------------------------------------------------------------------
# | 宝塔Windows面板
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved.
# +-------------------------------------------------------------------
# | Author: 沐落 <cjx@bt.cn>
# +-------------------------------------------------------------------

import win32serviceutil,win32service,win32event
import os,sys,ssl,psutil,time,threading,subprocess

class btService(win32serviceutil.ServiceFramework):
    #服务名
    _svc_name_ = "btPanel"
    #服务在windows系统中显示的名称
    _svc_display_name_ = "btPanel"
    #服务的描述
    _svc_description_ = "用于运行宝塔Windows面板主程序,停止后面板将无法访问."
    _svc_reg_class = "tmp.reg_class"

    proc = None
    def __init__(self, args):

        win32serviceutil.ServiceFramework.__init__(self, args)
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
        self.run = True


    def SvcDoRun(self):

        path = os.getenv('BT_PANEL')
        os.chdir(path)
        sys.path.insert(0,path + "/class/")
        import public

        p_file = '{}/logs/panel.pid'.format(path)

        try:
            pid = public.readFile(p_file).strip()
            if pid: os.system("taskkill /t /f /pid {}".format(pid))
        except: pass
        if os.path.exists(p_file): os.remove(p_file)

        import subprocess
        self.proc = subprocess.Popen(['C:\Program Files\python\python.exe', path + '\\runserver.py'])
        public.writeFile(p_file,str(self.proc.pid))

        #添加守护，防止进程被中断
        while self.run:
            pro = None
            try:
                pro = psutil.Process(self.proc.pid)
            except : pass
            if not pro: self.run = False
            time.sleep(1)

        if self.run:
            win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)


    def SvcStop(self):
        self.run = False
        p_file = '{}/logs/panel.pid'.format(os.getenv('BT_PANEL'))
        try:
            os.system("taskkill /t /f /pid %s" % self.proc.pid)
        except :
            pid = public.readFile(p_file)
            if pid: os.system("taskkill /t /f /pid {}".format(pid))
        if os.path.exists(p_file): os.remove(p_file)
        # 先告诉SCM停止这个过程
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        # 设置事件
        win32event.SetEvent(self.hWaitStop)


#检查端口
def check_port(PORT,p = 0):
    try:
        import socket,re
        s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        s.settimeout(0.15)
        s.connect(("127.0.0.1",PORT))
        s.close()

        ret = public.ExecShell("netstat -aon|findstr \"{}\"".format(PORT))
        tmps = re.search("TCP\s+0.0.0.0:{}\s+.+LISTENING\s+(\d+)".format(PORT),ret[0])

        result = "检测到面板使用的【{}】端口被以上进程占用，请解除占用后重启面板\n".format(PORT)
        if tmps:
            pid = int(tmps.groups()[0])
            proc = psutil.Process(pid)

            start_name = proc.username()
            try:
                start_name = proc.username().split('\\')[1]
            except :pass

            result += "\n"
            result += ' 进程PID  ：{}\n'.format(pid)
            result += ' 进程名称 ：{}\n'.format(proc.name())
            result += ' 启动用户 ：{}\n'.format(start_name)
            result += ' 启动时间 ：{}\n'.format(public.format_date('%Y-%m-%d %H:%M:%S',proc.create_time()))

            if p == 0:
                os.system("taskkill /t /f /pid {}".format(pid))
                return check_port(PORT,1)

        else:
            result += ('|-- {}\r\n'.format(ret[0]))
        return result
    except :
        return False

def run_flask(HOST, PORT, is_debug = False):
    path = os.getenv('BT_PANEL')
    is_ssl = os.path.exists(path + '/data/ssl.pl')

    if is_ssl:
        keyfile = 'ssl/privateKey.pem'
        certfile = 'ssl/certificate.pem'

        ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        ssl_context.load_cert_chain(certfile=certfile,keyfile=keyfile)
        ssl_context.options = (ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1)
        ssl_context.set_ciphers("ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE")

        sfile = path + '/data/ssl_verify.pl'
        is_ssl_verify = os.path.exists(sfile)
        if is_ssl_verify:
            crlfile = 'ssl/crl.pem'
            rootcafile = 'ssl/ca.pem'

            if not os.path.exists(crlfile) or os.path.exists(rootcafile):
                os.remove(sfile)
            else:
                #注销列表
                ssl_context.load_verify_locations(crlfile)
                ssl_context.verify_flags |= ssl.VERIFY_CRL_CHECK_CHAIN
                #加载证书
                ssl_context.load_verify_locations(rootcafile)
                ssl_context.verify_mode = ssl.CERT_REQUIRED
                ssl_context.set_default_verify_paths()

    if is_debug:
        if is_ssl:
            app.run(host = HOST,port = PORT,debug = True,ssl_context = ssl_context)
        else:
            app.run(host = HOST,port = PORT,debug = True,threaded=True)
    else:
        from gevent import pywsgi
        try:
            from geventwebsocket.handler import WebSocketHandler
        except :
            os.system(public.get_run_pip('[PIP] install gevent-websocket'))
            from geventwebsocket.handler import WebSocketHandler

        print("Starting on http://{0:s}:{1:d}".format(HOST, PORT))
        if is_ssl:
            server = pywsgi.WSGIServer((HOST, PORT),app,handler_class=WebSocketHandler,ssl_context = ssl_context)
        else:
            server = pywsgi.WSGIServer((HOST, PORT),app,handler_class=WebSocketHandler)
        server.serve_forever()

if __name__ == '__main__':
    path = os.getenv('BT_PANEL')

    if len(sys.argv) >= 2:
        if not path:
            path = os.path.dirname(sys.argv[0])
            os.environ['BT_PANEL'] = path
            if not path:
                print('ERROR:安装失败，找不到环境变量【BT_PANEL】.')
                exit()
        win32serviceutil.HandleCommandLine(btService)
    else:
        if not path:
            path = os.path.dirname(sys.argv[0])
            os.environ['BT_PANEL'] = path

        is_debug = os.path.exists(path + '/data/debug.pl')
        if not is_debug:
            #必须放在前面，否则requests模块无效
            try:
                from gevent import monkey
            except :
                os.system('"C:\Program Files\python\python.exe" -m pip install gevent -I')
                from gevent import monkey
            monkey.patch_all()

        os.chdir(path)
        sys.path.insert(0,path + "/class/")
        from os import environ

        from BTPanel import app,sys
        import public

        PORT = 8888
        filename = path + '/data/port.pl'
        if os.path.exists(filename):
            PORT = int(public.readFile(filename))
        HOST = '0.0.0.0'

        result =  check_port(PORT)
        if result:exit(result)

        #子进程
        subprocess.Popen(['C:\Program Files\python\python.exe', '{}/class/jobs.py'.format(public.get_panel_path())])

        subprocess.Popen(['C:\Program Files\python\python.exe', '{}/script/reload_check.py'.format(public.get_panel_path())])
        if os.path.exists(path + '/data/ipv6.pl'):
            #开发者模式无法同时启用IPv4和IPv6.
            if not is_debug:
                p = threading.Thread(target = run_flask, args = ('::', PORT, is_debug))
                p.setDaemon(True)
                p.start()

        run_flask(HOST, PORT, is_debug)

