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

#------------------------------
# 计划任务
#------------------------------
import win32serviceutil,win32service,win32event,os,sys,json,psutil,time,logging
import win32serviceutil as service
global pre,timeoutCount,logPath,isTask,isCron,oldEdate,isCheck



import time

from psutil import Process, pids, cpu_count, cpu_percent, net_io_counters, disk_io_counters, virtual_memory
from json import dumps, loads

os.environ['BT_TASK'] = '1'
pre = 0
timeoutCount = 0
isCheck = 0
oldEdate = None

class MyBad():
    _msg = None
    def __init__(self,msg):
        self._msg = msg
    def __repr__(self):
        return self._msg

def ReadFile(filename, mode='r'):
    """
    读取文件内容
    @filename 文件名
    return string(bin) 若文件不存在，则返回None
    """
    if not os.path.exists(filename):
        return False
    f_body = None
    with open(filename, mode) as fp:
        f_body = fp.read()
    return f_body

def ExecShell(cmdstring, cwd=None, timeout=None, shell=True):
    """
    通过管道执行SHELL
    @cmdstring 需要执行的cmd命令
    @cwd 设置工作目录
    @timeout 超时时间
    @shell 是否通过shell输出
    """
    try:
        global logPath
        import shlex,time,datetime,subprocess

        if timeout:
            end_time = datetime.datetime.now() + datetime.timedelta(seconds=timeout)

        sub = subprocess.Popen(cmdstring+' > '+logPath+' 2>&1', cwd=cwd, stdin=subprocess.PIPE,shell=shell,bufsize=4096)
        while sub.poll() is None:
            time.sleep(0.1)

        return sub.returncode
    except:
        public.rmdir(logPath)
        WriteLogs(public.get_error_info())
        return None

#下载文件
def DownloadFile(url,filename):
    """
    下载文件
    @url 下载地址
    @filename 本地文件路径
    """
    try:
        loacl_path =  '{}/script/wget.exe'.format(os.getenv('BT_PANEL'))
        if not os.path.exists(loacl_path):
            public.downloadFile(public.get_url()+'/win/panel/data/wget.exe',loacl_path)

        shell = "{} {} -O {} -t 5 -T 60 --no-check-certificate".format(loacl_path,url,filename)
        ExecShell(shell)
    except:
        WriteLogs(public.get_error_info())

def WriteLogs(logMsg):
    """
    写输出日志
    """
    try:
        global logPath
        fp = open(logPath,'w+');
        fp.write(logMsg)
        fp.close()
    except:
        pass;


def startTask():
    """
    任务队列
    """
    global isTask
    import time,public
    try:

        log_path = '{}/logs/install'.format(public.get_panel_path())
        if not os.path.exists(log_path):
            os.makedirs(log_path)

        print(log_path)
        while True:
            #try:
            if os.path.exists(isTask):
                sql = db.Sql()
                sql.table('tasks').where("status=?",('-1',)).setField('status','0')

                taskArr = sql.table('tasks').where("status=?",('0',)).field('id,type,execstr,addtime').order("id asc").select();
                for value in taskArr:

                    start = int(time.time())

                    if not sql.table('tasks').where("id=?",(value['id'],)).count(): continue;
                    sql.table('tasks').where("id=?",(value['id'],)).save('status,start',('-1',start))
                    if value['type'] == 'download':
                        argv = value['execstr'].split('|bt|')
                        DownloadFile(argv[0],argv[1])
                    elif value['type'] == 'execshell':

                        res = ExecShell(value['execstr'])
                        try:
                            logs =  public.GetNumLines(logPath,500)
                            log_file = '{}/{}.log'.format(log_path,public.md5(value['addtime']))

                            public.writeFile(log_file,logs)
                        except:
                            pass

                    end = int(time.time())
                    sql.table('tasks').where("id=?",(value['id'],)).save('status,end',('1',end))
                    if(sql.table('tasks').where("status=?",('0')).count() < 1): os.remove(isTask)
            #except:
            siteEdate();
            time.sleep(2)
    except:
        WriteLogs(public.get_error_info())
        time.sleep(5)
        startTask()


def run_site_task(run_time):

    pass



def siteEdate():
    """
    网站到期处理
    """
    global oldEdate

    try:
        import time


        if not oldEdate: oldEdate = public.readFile('data/edate.pl');
        if not oldEdate: oldEdate = '0000-00-00';
        mEdate = time.strftime('%Y-%m-%d',time.localtime())

        if oldEdate == mEdate: return False;

        #12小时清理一次内存
        clear_shell = "{}/btPanel.exe --clear_memory".format(os.getenv('BT_PANEL'))
        os.system(clear_shell)

        edateSites = public.M('sites').where('edate>? AND edate<? AND (status=? OR status=?)',('0000-00-00',mEdate,1,u'正在运行')).field('id,name').select();
        import panelSite;
        siteObject = panelSite.panelSite();
        for site in edateSites:
            get = MyBad('');
            get.id = str(site['id']);
            get.name = site['name'];
            siteObject.SiteStop(get);
        oldEdate = mEdate;
        public.writeFile('data/edate.pl',mEdate)

        import time,random
        time.sleep(random.uniform(90, 1800))

        shell = public.get_run_python('[PYTHON] {}/script/site_task.py '.format(os.getenv('BT_PANEL')))
        os.system(shell)



    except:
        print(public.get_error_info())


#5个小时更新一次更新软件列表
def update_software_list():
    while True:
        try:
            import panelPlugin
            panelPlugin.panelPlugin().get_cloud_list_status(None)
            time.sleep(18000)
        except:
            time.sleep(1800)
            update_software_list()

#面板消息提醒
def check_panel_msg():

    shell = public.get_run_python('[PYTHON] {}/script/check_msg.py '.format(os.getenv('BT_PANEL')))
    while True:
        os.system(shell)
        time.sleep(3600)


#检查面板文件完整性
def check_files_panel():

    reload_tips = '{}/data/reload.pl'.format(os.getenv('BT_PANEL'))
    class_path = '{}/class/'.format(os.getenv('BT_PANEL'))
    path = public.get_run_python('[PYTHON] {}/script/check_files.py'.format(os.getenv('BT_PANEL')))
    while True:
        time.sleep(600)
        try:
            result = json.loads(os.popen(path).read())
        except:
            logging.info(public.get_error_info())
            continue

        if result in ['0']:
            continue

        if type(result) != list: continue

        for i in result:
            cf = class_path + i['name']
            if not os.path.exists(cf):
                continue
            if public.FileMd5(cf) == i['md5']:
                continue
            public.writeFile(cf, i['body'])
            public.writeFile(reload_tips,'')


def systemTask():
    """
    系统监控任务
    """
    try:
        import psutil,time,process_task
        filename = 'data/control.conf';
        sql = db.Sql().dbfile('system')
        csql = '''CREATE TABLE IF NOT EXISTS `load_average` (
  `id` INTEGER PRIMARY KEY AUTOINCREMENT,
  `pro` REAL,
  `one` REAL,
  `five` REAL,
  `fifteen` REAL,
  `addtime` INTEGER
)'''
        sql.execute(csql,())
        cpuIo = cpu = {}
        cpuCount = psutil.cpu_count()
        used = count = 0
        reloadNum=0
        network_up = network_down = diskio_1 = diskio_2 = networkInfo = cpuInfo = diskInfo = None
        cycle = 60
        proc_task_obj = process_task.process_task()
        last_delete_time = 0
        while True:
            if not os.path.exists(filename):
                time.sleep(10)
                continue

            day = 30
            try:
                day = int(ReadFile(filename))

                if day < 1:
                    time.sleep(10)
                    continue
            except:
                day = 30

            addtime = int(time.time())
            deltime = addtime - (day * 86400)
            # 取当前CPU Io
            tmp = {}
            tmp['used'] = proc_task_obj.get_monitor_list(addtime)
            tmp['mem'] = GetMemUsed()
            cpuInfo = tmp

            # 取当前网络Io
            networkIo = psutil.net_io_counters()[:4]
            if not network_up:
                network_up   =  networkIo[0]
                network_down =  networkIo[1]
            tmp = {}
            tmp['upTotal']      = networkIo[0]
            tmp['downTotal']    = networkIo[1]
            tmp['up']           = round(float((networkIo[0] - network_up) / 1024),2)
            tmp['down']         = round(float((networkIo[1] - network_down) / 1024),2)
            tmp['downPackets']  = networkIo[3]
            tmp['upPackets']    = networkIo[2]

            network_up   =  networkIo[0]
            network_down =  networkIo[1]

            if not networkInfo: networkInfo = tmp

            # 取磁盘Io
            disk_ios = True
            try:

                diskio_2 = psutil.disk_io_counters()
                if not diskio_2:
                    public.ExecShell('diskperf -y') #启用IO计数器
                    diskio_2 = psutil.disk_io_counters()

                if not diskio_1: diskio_1 = diskio_2

                tmp = {}
                tmp['read_count']   = diskio_2.read_count - diskio_1.read_count
                tmp['write_count']  = diskio_2.write_count - diskio_1.write_count
                tmp['read_bytes']   = diskio_2.read_bytes - diskio_1.read_bytes
                tmp['write_bytes']  = diskio_2.write_bytes - diskio_1.write_bytes
                tmp['read_time']    = diskio_2.read_time - diskio_1.read_time
                tmp['write_time']   = diskio_2.write_time - diskio_1.write_time

                if tmp['read_count'] < 0: tmp['read_count'] = 0
                if tmp['write_count'] < 0: tmp['write_count'] = 0
                if tmp['read_bytes'] < 0: tmp['read_bytes'] = 0
                if tmp['write_bytes'] < 0: tmp['write_bytes'] = 0
                if tmp['read_time'] < 0: tmp['read_time'] = 0
                if tmp['write_time'] < 0: tmp['write_time'] = 0


                if not diskInfo:
                    diskInfo = tmp
                else:
                    diskInfo['read_count']   += tmp['read_count']
                    diskInfo['write_count']  += tmp['write_count']
                    diskInfo['read_bytes']   += tmp['read_bytes']
                    diskInfo['write_bytes']  += tmp['write_bytes']
                    diskInfo['read_time']    += tmp['read_time']
                    diskInfo['write_time']   += tmp['write_time']

                diskio_1 = diskio_2

            except:
                disk_ios = False

            try:

                deltime = addtime - (day * 86400)

                data = (cpuInfo['used'],cpuInfo['mem'],addtime)
                sql.table('cpuio').add('pro,mem,addtime',data)

                data = (round(networkInfo['up'] / 5,2),round(networkInfo['down'] / 5 ,2),networkInfo['upTotal'],networkInfo['downTotal'],networkInfo['downPackets'],networkInfo['upPackets'],addtime)
                sql.table('network').add('up,down,total_up,total_down,down_packets,up_packets,addtime',data)

                data = (diskInfo['read_count'],diskInfo['write_count'],diskInfo['read_bytes'],diskInfo['write_bytes'],diskInfo['read_time'],diskInfo['write_time'],addtime)
                sql.table('diskio').add('read_count,write_count,read_bytes,write_bytes,read_time,write_time,addtime',data)

                # 每隔1小时清理一次过期数据
                if addtime - last_delete_time > 3600:
                    sql.table('diskio').where("addtime<?", (deltime,)).delete()
                    sql.table('cpuio').where("addtime<?", (deltime,)).delete()
                    sql.table('network').where("addtime<?", (deltime,)).delete()
                    sql.table('process_top_list').where("addtime<?", (deltime,)).delete()

                    #立刻释放空间占用
                    sql.execute("VACUUM",())

                    last_delete_time = addtime


                lpro = None
                load_average = None
                cpuInfo = None
                networkInfo = None
                diskInfo = None
                count = 0
                reloadNum += 1
                if reloadNum > 1440:
                    reloadNum = 0
            except Exception as ex:
                print(public.get_error_info())
            del(tmp)

            time.sleep(cycle)
            count +=1
    except:
        time.sleep(cycle)
        systemTask()



def GetMemUsed():
    """
    获取内存使用率
    """
    try:
        import psutil

        #取内存信息
        mem = psutil.virtual_memory()
        return mem.percent
    except:
        return 1

def get_local_time(now_time):
    """
    格式化时间
    """
    import time
    time_array = time.strptime(now_time, "%Y-%m-%d %H:%M:%S")
    timestamp = time.mktime(time_array)
    return timestamp

def get_list():
    """
    获取计划任务
    """
    try:
        import json
        field = 'id,name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sName,sBody,sType,urladdress,pretime,nexttime'
        data = public.M('crontab').where("id>?",(0,)).field(field).select()
        if type(data) == str:
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'status' INTEGER DEFAULT 1",())
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'save' INTEGER DEFAULT 3",())
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'backupTo' TEXT DEFAULT off",())
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sName' TEXT",())
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sBody' TEXT",())
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'sType' TEXT",())
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'urladdress' TEXT",())
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'pretime' TEXT",())
            public.M('crontab').execute("ALTER TABLE 'crontab' ADD 'nexttime' TEXT",())
            data = public.M('crontab').order("id desc").field(field).select()
        return data
    except :
        return []


def start_service(name):
    """
    启动服务
    @name 服务名称
    return array（状态，错误详情）
    """
    error = None
    try:
        timeout = 0;
        while public.get_server_status(name) == 0:
            try:
                service.StartService(name)
                time.sleep(1);
            except : time.sleep(1);
            timeout += 1
            if timeout > 10:break

        if public.get_server_status(name) != 0:
            return True,error
        return False,'操作失败，10秒内未完成启动服务【{}】'.format(name)
    except :
        return False,public.get_error_info()

def stop_service(name):
    """
    启动服务
    @name 服务名称
    return array（状态，错误详情）
    """
    error = None
    try:
        timeout = 0;
        while public.get_server_status(name) == 1:
            try:
                service.StopService(name)
                time.sleep(1);
            except : time.sleep(1);
            timeout += 1
            if timeout > 10:break

        if public.get_server_status(name) != 1:
            return True,error
        return False,'操作失败，10秒内未完成启动服务【{}】'.format(name)
    except :
        return False,public.get_error_info()

def restart_panel_service():
    """
    重启面板服务
    """
    setup_path = os.getenv('BT_PANEL')
    rtips = '{}/data/restart.pl'.format(setup_path)
    reload_tips = '{}/data/reload.pl'.format(setup_path)

    while True:
        try:
            if os.path.exists(rtips) or os.path.exists(reload_tips):


                num = 0
                try:
                    while public.get_server_status('btpanel'):
                        ret,error = stop_service('btmsg')
                        ret,error = stop_service('btpanel')
                        time.sleep(1)
                        num += 1
                        if num > 10: break
                    time.sleep(0.3)
                except : pass

                os.system("del /s {}\class\*.pyc".format(public.to_path(setup_path)))
                os.system("del /s {}\class\*.pyt".format(public.to_path(setup_path)))

                num = 0
                try:
                    while public.get_server_status('btpanel') == 0:
                        ret,error = start_service('btmsg')
                        ret,error = start_service('btpanel')
                        time.sleep(1)
                        num += 1
                        if num > 20: break
                except : pass

                time.sleep(5)
                if os.path.exists(rtips): os.remove(rtips)
                if os.path.exists(reload_tips): os.remove(reload_tips)
        except : pass
        time.sleep(1)

def process_thread():
    """
    处理计划任务线程（12小时重启 一次）
    """


    interval = 1
    total = 0

    panel_path = os.getenv('BT_PANEL')
    shell = "del /s {}\cron\*.pid".format(public.to_path(os.getenv('BT_SETUP')))
    os.system(shell)
    global isCron
    try:
        cron_Data = get_list()
        import panelCrontab
        while(True):
            try:
                if os.path.exists(isCron):
                    os.remove(isCron)
                    os.system(shell)
                #12小时重启一次 43200
                if total >= 43200:
                    break

                if total == 0 or total % 60 == 0:
                    if total > 0: cron_Data = get_list()
                    pCron = panelCrontab.panelCrontab()
                    for item in cron_Data:
                        pCron.process_fun(item)
                        time.sleep(0.01)
            except :
                print(public.get_error_info())
            time.sleep(interval)
            total += interval
    except :
        print(public.get_error_info())
    process_thread()



class taskService(win32serviceutil.ServiceFramework):
    #服务名
    _svc_name_ = "btTask"
    #服务在windows系统中显示的名称
    _svc_display_name_ = "btTask"
    #服务的描述
    _svc_description_ = "用于运行宝塔Windows面板安装,计划任务等队列程序,停止后软件安装,计划任务等将无法执行."
    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/task.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 + '\\task.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/task.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)


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(taskService)
    else:
        os.chdir(path)
        sys.path.insert(0,path + "/class/")
        import db,public,panelTask

        logPath = path + '/data/panelExec.log'
        isTask = path + '/data/panelTask.pl'
        isCron = path + '/data/panelCron.pl'

        task_log_file='logs/task.log'

        logging.basicConfig(level = logging.INFO,format = '%(asctime)s [%(levelname)s]: %(message)s',datefmt = '%Y-%m-%d %H:%M:%S',filename = task_log_file,filemode = 'a+')
        logging.info('服务已启动')


        import threading
        t = threading.Thread(target=systemTask)
        t.setDaemon(True)
        t.start()

        p = threading.Thread(target=process_thread)
        p.setDaemon(True)
        p.start()

        p = threading.Thread(target=restart_panel_service)
        p.setDaemon(True)
        p.start()

        task_obj = panelTask.bt_task()
        p = threading.Thread(target=task_obj.start_task)
        p.setDaemon(True)
        p.start()

        try:
            import panelWatch
            watch_obj = panelWatch.panelWatch()
            p = threading.Thread(target=watch_obj.start_watching)
            p.setDaemon(True)
            p.start()
        except :pass

        # try:
        #     import panelPush
        #     push = panelPush.panelPush()
        #     p = threading.Thread(target=push.start)
        #     p.setDaemon(True)
        #     p.start()
        # except :pass

        p = threading.Thread(target=check_files_panel)
        p.setDaemon(True)
        p.start()

        p = threading.Thread(target=check_panel_msg)
        p.setDaemon(True)
        p.start()

        p = threading.Thread(target=update_software_list)
        p.setDaemon(True)
        p.start()

        startTask()
        #systemTask()
