#!/usr/bin/env python3 """ This script provides a PYNQ control panel. This is intended to run on the host server (Raspberry Pi). """ import argparse import datetime import glob import json import os import re import requests import subprocess import sys import threading import time from collections import OrderedDict from flask import Flask, render_template, request, g from gevent import pywsgi, monkey from geventwebsocket.handler import WebSocketHandler from queue import Queue sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../../solver') import BoardStr import adcclilib app = Flask(__name__) monkey.patch_all() app_args = {} questions = None clients = None current_seed = 1 wsq = {} def load_questions(): global app_args global questions question_path = os.path.abspath(app_args["question"]) question_list = glob.glob("{}/NL_*.txt".format(question_path)) question_list.sort() questions = OrderedDict() for v in question_list: # 問題の情報を読み込む with open(v, "r") as fp: _q_lines = fp.readlines() board_size = "" line_num = -1 for l in _q_lines: if "SIZE" in l: board_size = l.strip().split()[1] if "LINE_NUM" in l: line_num = int(l.strip().split()[1]) break _name = os.path.basename(v) questions[_name] = { "path": v, "status": "Not solved", "answer": "", "board_size": board_size, "line_num": line_num, "last_req": None, "solver": OrderedDict(), "answers": OrderedDict(), } update_answer_list(_name) # 既に回答されているものを読み込む answer_path = os.path.abspath(app_args["out"]) answer_list = glob.glob("{}/submit/T01_A*.txt".format(answer_path)) for v in answer_list: _ans_name = os.path.basename(v) m = re.search(r"T01_A([0-9A-Za-z]+)\.txt", _ans_name) if m: _name = "NL_Q{}.txt".format(m.group(1)) with open(v, "r") as fp: _ans_dat = fp.read() try: questions[_name]["status"] = "Saved" questions[_name]["answer"] = _ans_dat except KeyError as e: pass def update_answer_list(qname): global app_args global questions global clients questions[qname]['solver'] = OrderedDict() questions[qname]['answers'] = OrderedDict() for c in clients['solver']: questions[qname]['solver'][c[0]] = "-" _folder_name = os.path.splitext(qname)[0] _answers_path = "{}/{}".format(app_args['out'], _folder_name) if os.path.isdir(_answers_path): answer_log_file = glob.glob("{}/*.json".format(_answers_path)) answer_log_file.sort() answer_log_file.reverse() is_solved = False for v2 in answer_log_file: json_name = os.path.basename(v2) with open(v2, "r") as fp: answer_log = json.load(fp) questions[qname]['answers'][json_name] = answer_log if answer_log['solved'] == "True": is_solved = True if questions[qname]['answer'] == answer_log['answer']: questions[qname]['best_json'] = json_name if (questions[qname]['last_req'] == float(answer_log['req_id'])): if answer_log['solver'] in questions[qname]['solver']: if answer_log['solved'] == "True": questions[qname]['solver'][answer_log['solver']] = "Solved" else: questions[qname]['solver'][answer_log['solver']] = "DNS" if is_solved > 0: questions[qname]['status'] = "Solved" @app.before_request def before_request(): global app_args if app_args["force_local_mode"]: g.local_mode = True else: g.local_mode = (request.remote_addr in ["127.0.0.1", "::1"]) @app.route("/post", methods=["POST"]) def post(): global questions global app_args global wsq _qname = request.form["qname"] _answer = request.form["solution"] _client = request.form["client"] _req_id = request.form["req_id"] _is_solved = request.form["solved"] if "solver" in request.form: _solver = request.form["solver"] else: _solver = _client receive_time = datetime.datetime.now() receive_time_str = receive_time.strftime("%Y%m%d%H%M%S") dat = { "req_id": _req_id, "qname": _qname, "client": _client, "solver": _solver, "answer": _answer, "cputime": request.form['cputime'], "timestamp": receive_time.strftime("%Y/%m/%d %H:%M:%S"), "nlcheck": -1, "solved": _is_solved, } if _is_solved == "True": # Create a temporary file to execute "nlcheck.py" tmp_path = "{}/{}".format(app_args['out'], "tmp") if not os.path.isdir(tmp_path): os.makedirs(tmp_path) tmp_file_name = "{}-{}.tmp".format(receive_time_str, _client.replace(":", ".")) tmp_file_path = "{}/{}".format(tmp_path, tmp_file_name) with open(tmp_file_path, "w") as fp: fp.write(_answer) probpath = "{}/{}".format(app_args["question"], _qname) # Format check cmd = "/usr/bin/python /home/pi/adc2018/conmgr/adc2018/server/nlcheck.py --input {} --target {}".format(probpath, tmp_file_path) print("`{}`".format(cmd)) p = subprocess.run(cmd.strip().split(" "), stdout=subprocess.PIPE) nlcheck = p.stdout.decode().strip() pattern = r"judge += +\[(.+?), ([0-9.]+)\]" m = re.match(pattern, nlcheck) if m and (m.group(1) == "True"): dat["nlcheck"] = float(m.group(2)) save_path = "{}/{}".format(app_args['out'], os.path.splitext(_qname)[0]) if not os.path.isdir(save_path): os.makedirs(save_path) file_name = "{}-{}.json".format(receive_time_str, _solver.replace(":", ".")) save_file_path = "{}/{}".format(save_path, file_name) with open(save_file_path, "w") as fp: json.dump(dat, fp, indent=4) else: save_path = "{}/{}".format(app_args['out'], os.path.splitext(_qname)[0]) if not os.path.isdir(save_path): os.makedirs(save_path) file_name = "{}-{}.json".format(receive_time_str, _solver.replace(":", ".")) save_file_path = "{}/{}".format(save_path, file_name) with open(save_file_path, "w") as fp: json.dump(dat, fp, indent=4) for k, v in wsq.items(): if v is not None: v.put(dat) res = {"status": "OK"} return json.dumps(res) @app.route("/start", methods=["POST"]) def start(): global app_args global questions global current_seed _question_name = request.form["qname"] _q = questions[_question_name] questions[_question_name]["status"] = "Processing" with open(_q["path"], "r") as fp: _q_lines = fp.readlines() line_num = -1 for l in _q_lines: if "LINE_NUM" in l: line_num = int(l.strip().split()[1]) break qstr = "\n".join(_q_lines) res = {} if line_num >= 0: # LINE_NUMが閾値未満のとき,PYNQに問題を配信して問題を解かせる res = solve_questions(_question_name, qstr) # 最終結果だけを保存 questions[_question_name]["status"] = res["status"] return json.dumps(res) def solve_questions(qname, qstr): """ このメソッドでは,問題データをソルバのクライアントに送りつける. 結果は/postに送られてくるので,結果の集計はそちらで行う. """ global clients global questions global current_seed def worker(host, qname, qstr, qseed, req_id, resolver): _url = "http://{}/start".format(host) data = { "client": host, "qname": qname, "question": qstr, "qseed": qseed, "req_id": req_id, "resolver": resolver } try: r = requests.post(_url, data=data) client_res = json.loads(r.text) except Exception as e: sys.stderr.write(str(e) + "\n") threads = [] # 時刻をリクエストIDとして設定 req_id = time.time() questions[qname]['last_req'] = req_id for c in clients['solver']: # 問題はSolverに送る client_addr = c[0] idx_of_resolver = current_seed % len(clients['resolver']) resolver = clients['resolver'][idx_of_resolver][0] _th = threading.Thread(name=client_addr, target=worker, args=(client_addr, qname, qstr, current_seed, req_id, resolver)) _th.start() threads.append(_th) current_seed += 1 res = {"status": "Processed"} return res @app.route("/stop", methods=["POST"]) def stop(): _client = request.form["client"] _url = _client + "/stop" try: r = requests.get(_url) client_res = json.loads(r.text)["status"] except Exception as e: client_res = "Connection error" sys.stderr.write(str(e) + "\n") res = {"status": client_res} return json.dumps(res) @app.route("/status", methods=["POST"]) def get_status(): _client = request.form["client"] _url = _client + "/status" try: r = requests.get(_url) client_res = json.loads(r.text)["status"] except Exception as e: client_res = "Connection error" sys.stderr.write(str(e) + "\n") res = {"status": client_res} return json.dumps(res) @app.route("/save", methods=["POST"]) def save_best_solution(): """ それぞれの問題に対し,解き終わった答えの中で最も品質の高い解を submitフォルダに出力する. """ global questions global app_args qname = request.form["qname"] _best_score = -1 _best_json = "" for k, v in questions[qname]['answers'].items(): try: _score = float(v['nlcheck']) except: _score = -1 if _score > _best_score: _best_score = _score _best_json = k if _best_json != "": out_path = "{}/{}".format(app_args['out'], "submit") if not os.path.isdir(out_path): os.makedirs(out_path) print("Saved answer for {}: {}".format(qname, _best_json)) qnumber = qname.replace("NL_Q", "").replace(".txt", "") out_file_path = "{}/T01_A{}.txt".format(out_path, qnumber) with open(out_file_path, 'w') as fp: fp.write(questions[qname]['answers'][_best_json]['answer']) questions[qname]["status"] = "Saved" questions[qname]['best_json'] = _best_json res = {"status": "OK"} else: res = {"status": "No answer is found"} return json.dumps(res) @app.route("/board-data", methods=["POST"]) def get_board_data(): global questions qname = request.form['qname'] json_name = request.form['jname'] lines = questions[qname]['answers'][json_name]['answer'].split("\n") quality = questions[qname]['answers'][json_name]['nlcheck'] # board_size = [int(v) for v in lines[1].rstrip().split()[1].split("X")] board_size = [int(v) for v in questions[qname]['board_size'].split("X")] raw_data = [[[0 for x in range(board_size[0])] for y in range(board_size[1])] for z in range(board_size[2])] data = [] max_line_num = 0 for l in lines[1:]: if l.rstrip() == "": continue elif l.startswith("LAYER"): z = int(l.rstrip().split()[1])-1 y = 0 continue else: ldat = l.rstrip().split(",") for x, v in enumerate(ldat): raw_data[z][y][x] = int(v) max_line_num = max(max_line_num, int(v)) y += 1 terminals = [[] for v in range(max_line_num+1)] for z in range(board_size[2]): for y in range(board_size[1]): for x in range(board_size[0]): line_num = raw_data[z][y][x] if line_num == 0: continue adj_cnt = 0 if((0 <= x-1 < board_size[0]) and (raw_data[z][y][x-1] == line_num)): adj_cnt += 1 if((0 <= x+1 < board_size[0]) and (raw_data[z][y][x+1] == line_num)): adj_cnt += 1 if((0 <= y-1 < board_size[1]) and (raw_data[z][y-1][x] == line_num)): adj_cnt += 1 if((0 <= y+1 < board_size[1]) and (raw_data[z][y+1][x] == line_num)): adj_cnt += 1 if((0 <= z-1 < board_size[2]) and (raw_data[z-1][y][x] == line_num)): adj_cnt += 1 if((0 <= z+1 < board_size[2]) and (raw_data[z+1][y][x] == line_num)): adj_cnt += 1 if adj_cnt == 1: terminals[line_num].append((x, y, z)) lines = {} for i in range(1, max_line_num+1): if len(terminals[i]) != 2: continue start = terminals[i][0] goal = terminals[i][1] lines[i] = [] lines[i].append(start) lines[i].append(start) now = start _next = (0, 0, 0) while now != goal: x, y, z = now adj_cnt = 0 if((0 <= x-1 < board_size[0]) and (raw_data[z][y][x-1] == i) and (lines[i][-2] != (x-1, y, z))): _next = (x-1, y, z) elif((0 <= x+1 < board_size[0]) and (raw_data[z][y][x+1] == i) and (lines[i][-2] != (x+1, y, z))): _next = (x+1, y, z) elif((0 <= y-1 < board_size[1]) and (raw_data[z][y-1][x] == i) and (lines[i][-2] != (x, y-1, z))): _next = (x, y-1, z) elif((0 <= y+1 < board_size[1]) and (raw_data[z][y+1][x] == i) and (lines[i][-2] != (x, y+1, z))): _next = (x, y+1, z) elif((0 <= z-1 < board_size[2]) and (raw_data[z-1][y][x] == i) and (lines[i][-2] != (x, y, z-1))): _next = (x, y, z-1) elif((0 <= z+1 < board_size[2]) and (raw_data[z+1][y][x] == i) and (lines[i][-2] != (x, y, z+1))): _next = (x, y, z+1) else: print("Error: Not found an adjacent node") _next = goal lines[i].append(_next) now = _next res = { 'line': lines, 'board': board_size, 'quality': quality, } return json.dumps(res) @app.route("/get_clients") def get_clients(): global clients res = OrderedDict() for c in clients['all']: client_ip = c[0] res[client_ip] = "http://{}".format(client_ip) return json.dumps(res) @app.route("/adccli-login") def adccli_login(): global app_args with open(app_args['adccli'], 'r') as fp: d = json.load(fp) r = adcclilib.login(d['url'], d['username'], d['password']) res = {'message': r} return json.dumps(res) @app.route("/adccli-logout") def adccli_logout(): r = adcclilib.logout() res = {'message': r} return json.dumps(res) @app.route("/adccli-whoami") def adccli_whoami(): global app_args with open(app_args['adccli'], 'r') as fp: d = json.load(fp) r = adcclilib.whoami() res = {'message': r, 'status': r == d['username']} return json.dumps(res) @app.route("/adccli-get-all-q") def adccli_get_all_q(): global app_args out_abspath = os.path.abspath(app_args['question']) r = adcclilib.get_q_all(out_abspath) load_questions() res = {'message': r} return json.dumps(res) @app.route("/adccli-put-a", methods=["POST"]) def adccli_put_a(): global app_args global questions qname = request.form["qname"] if not "best_json" in questions[qname]: res = {'message': "Required to 'save' before submission"} else: out_path = "{}/{}".format(app_args['out'], "submit") qnumber = qname.replace("NL_Q", "").replace(".txt", "") ans_file_path = "{}/T01_A{}.txt".format(out_path, qnumber) ans_file_abspath = os.path.abspath(ans_file_path) r = adcclilib.put_a(qnumber, ans_file_abspath) mes = r + "\n" json_name = questions[qname]['best_json'] data = questions[qname]['answers'][json_name] r = adcclilib.put_a_info(qnumber, data['cputime'], "0", "Test") mes += r res = {'message': mes} return json.dumps(res) @app.route("/board-viewer", methods=["GET"]) def show_board_viewer(): return render_template("board-viewer.html") @app.route("/get_question_table") def question_table(): global app_args global questions question_path = os.path.abspath(app_args["question"]) return render_template("part_question_table.html", questions=questions, question_path=question_path) @app.route("/get_client_table") def client_table(): global app_args global clients return render_template("part_client_table.html", clients=clients["all"], local_mode=g.local_mode) @app.route("/get_question_status") def question_status(): global app_args global questions global clients qname = request.args.get("qname", "") update_answer_list(qname) qdata = questions[qname] return render_template("part_question_status.html", qname=qname, qdata=qdata, solvers=clients["solver"], localmode=g.local_mode) @app.route('/ws') def ws(): global wsq if request.environ.get('wsgi.websocket'): ws = request.environ['wsgi.websocket'] ws_id = time.time() wsq[ws_id] = Queue() while True: data = wsq[ws_id].get() str_data = json.dumps(data) ws.send(str_data) wsq[ws_id] = None @app.route("/") def index(): global app_args global questions question_path = os.path.abspath(app_args["question"]) return render_template("index.html", questions=questions, question_path=question_path) def main(args): raise NotImprementedError() def init_system(): global app_args global questions global clients global wsq if clients is None: with open(app_args["client"], "r") as fp: _clients = fp.readlines() all_clients = [v.rstrip().split() for v in _clients] solver = [] resolver = [] for v in all_clients: if v[1].lower() == "solver": solver.append(v) elif v[1].lower() == "resolver": resolver.append(v) clients = { "all": all_clients, "solver": solver, "resolver": resolver } if questions is None: load_questions() if __name__ == "__main__": parser = argparse.ArgumentParser(description="PYNQ control panel.") parser.add_argument("-c", "--client", action="store", type=str, default=None, required=True, help="Client list.") parser.add_argument("-a", "--adccli", action="store", type=str, default="./adccli.json", help="Path to the ADCCLI configuration json file.") parser.add_argument("-q", "--question", action="store", type=str, default="./problems", help="Path to the question folder.") parser.add_argument("-o", "--out", action="store", type=str, default="./answers", help="Path to the output folder.") parser.add_argument("--force-local-mode", action="store_true", default=False, help="Apply local mode view to all clients.") parser.add_argument("--debug", action="store_true", default=False, help="Debug mode.") args = vars(parser.parse_args()) app_args = args init_system() if args["debug"]: app.debug = True app.threaded = True # app.run(host='0.0.0.0', threaded=True) server = pywsgi.WSGIServer(("", 5000), app, handler_class=WebSocketHandler) server.serve_forever()