package com.educoder.bridge.controller; import com.alibaba.fastjson.JSONObject; import com.educoder.bridge.exception.GameException; import com.educoder.bridge.service.*; import com.educoder.bridge.settings.AppConfig; import com.educoder.bridge.utils.*; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.File; import java.text.SimpleDateFormat; /** * 和实训相关的接口 * * @author weishao * @date 2017/11/02 */ @Api(value = "game控制器", hidden = true) @RestController @RequestMapping("/game") public class GameController extends BaseController { @Autowired private AppConfig appConfig; @Autowired private GameService gameService; @Autowired private KubernetesService kubernetesService; @Autowired private K8sService k8sService; @Autowired private ThreadPoolTaskExecutor threadPoolTaskExecutor; private final static Logger logger = LoggerFactory.getLogger(GameController.class); /** * 开启实训: * 克隆版本库 * * @return */ @RequestMapping(path = "/openGameInstance") @ApiOperation(value = "开启实训", httpMethod = "POST", produces = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public JSONObject openGameInstance( @ApiParam(name = "tpmGitURL", required = true, value = "Tpm的gitUrl,需要base64编码") @RequestParam String tpmGitURL, @ApiParam(name = "tpiID", required = true, value = "实训实例的ID") @RequestParam String tpiID, @ApiParam(name = "tpiRepoName", required = true, value = "tpiRepoName") @RequestParam String tpiRepoName ) throws Exception { logger.info("tpmGitURL: {}\n, tpiID: {}, tpiRepoName: {}", tpmGitURL, tpiID, tpiRepoName); JSONObject response = new JSONObject(); // 设定工作路径为${workspace}/myshixun_${tpiID} String path = appConfig.getWorkspace() + File.separator + "myshixun_" + tpiID; // 对当前TPI,从TPM clone 版本库 tpmGitURL = Base64Util.decode(tpmGitURL); gameService.gitClone(path, tpmGitURL, "remote_origin", tpiRepoName); response.put("code", 0); response.put("msg", "开启成功"); return response; } /** * 评测: * 对每一次评测请求,先判断是否能立即执行还是需要先排队,如果能立即执行,开启一个线程执行以下步骤 * 如果需要排队,将相关参数封装成一个线程,存入redis队列,等待执行,并返回标志位给前台,通知前台进行轮询 * * 1. gitpull * 2. 如果有端口映射 * 3. 创建pod 和 service 并 创建定时删除pod的定时任务 * 4. 在pod中执行评测脚本 * 5. 回传结果 * * * @param tpiID * @param tpiGitURL * @param buildID * @param instanceChallenge * @param resubmit * @param times * @return * @throws Exception */ @RequestMapping(path = "/gameEvaluate") @ApiOperation(value = "评测", httpMethod = "POST", produces = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public JSONObject gameEvaluate( @ApiParam(name = "tpiID", required = true, value = "实训实例的ID") @RequestParam String tpiID, @ApiParam(name = "tpiGitURL", required = true, value = "学员对应当前实训的版本库地址,需要base64编码") @RequestParam String tpiGitURL, @ApiParam(name = "buildID", required = true, value = "本次评测ID") @RequestParam String buildID, @ApiParam(name = "instanceChallenge", required = true, value = "当前处在第几关") @RequestParam String instanceChallenge, @ApiParam(name = "testCases", required = true, value = "测试用例") @RequestParam String testCases, @ApiParam(name = "tpmScript", required = true, value = "tpm评测脚本,需要base64编码") @RequestParam String tpmScript, @ApiParam(name = "timeLimit", required = false, value = "时间限制") Integer timeLimit, @ApiParam(name = "resubmit", required = true, value = "是否是重复评测") @RequestParam String resubmit, @ApiParam(name = "times", required = true, value = "是否是时间轮询请求") @RequestParam Integer times, @ApiParam(name = "needPortMapping", required = false, value = "容器中需要被映射的端口") Integer needPortMapping, @ApiParam(name = "podType", required = true, value = "pod类型(0.evaluate,1.webssh,2.evassh)") @RequestParam Integer podType, @ApiParam(name = "file", required = false, value = "需要传文件的实训,给出文件存放路径(一个目录)及文件类型") String file, @ApiParam(name = "containers", required = true, value = "需要使用的容器,base64编码") @RequestParam String containers) throws Exception { // 记录开始时间 SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); logger.info("training_task_status start#1**{}**** {}", tpiID, dateformat.format(System.currentTimeMillis())); long evaStart = System.currentTimeMillis(); String evaluateStart = dateformat.format(evaStart); JSONObject cost = new JSONObject(); cost.put("evaluateStart", evaluateStart); cost.put("evaluateAllTime", evaStart); ConstantUtil.costMap.put(tpiID, cost); JSONObject response = new JSONObject(); // 所有构建需要的参数放到map中,供执行线程使用 tpiGitURL = Base64Util.decode(tpiGitURL); needPortMapping = needPortMapping == null ? 0 : needPortMapping; timeLimit = timeLimit == null ? Integer.parseInt(appConfig.getDefaultTimeLimit()) : timeLimit; testCases = Base64Util.decode(testCases); containers = Base64Util.decode(containers); // 每次评测均生成新TPI评测脚本 tpmScript = Base64Util.decode(tpmScript); String path = appConfig.getWorkspace() + File.separator + "myshixun_" + tpiID; String tpiRepoName = StringHelper.getRepoName(tpiGitURL); logger.info("generateTpiEvaluateShellScript start#2**{}**** {}", tpiID, dateformat.format(System.currentTimeMillis())); gameService.generateTpiEvaluateShellScript(tpmScript, path, tpiRepoName); logger.info("retryformat start#3**{}**** {}", tpiID, dateformat.format(System.currentTimeMillis())); JSONObject buildParams = new JSONObject(true); buildParams.put("tpiID", tpiID); buildParams.put("tpiGitURL", tpiGitURL); buildParams.put("buildID", buildID); buildParams.put("instanceChallenge", instanceChallenge); buildParams.put("testCases", testCases); buildParams.put("timeLimit", timeLimit); buildParams.put("resubmit", resubmit); buildParams.put("needPortMapping", needPortMapping); buildParams.put("podType", podType); buildParams.put("containers", containers); // 若实训生成文件 if (!StringUtils.isEmpty(file)) { file = Base64Util.decode(file); buildParams.put("file", file); JSONObject jsonObject = JSONObject.parseObject(file); // 清空目标文件夹以防止影响此次评测结果 String dir = path + "/" + tpiRepoName + "/" + jsonObject.getString("path"); StringHelper.deleteFiles(dir, "pic"); StringHelper.deleteFiles(dir, "apk"); StringHelper.deleteFiles(dir, "html"); } // 最大running pod数量 int maxRunningPodNum = Integer.parseInt(appConfig.getMaxRunningPodNum()); // podName String podName = podType == 0 ? "evaluate-" + tpiID : "evassh-" + tpiID; // 若需要端口映射服务,则分配端口,将podName-port键值对存储于redis String port = "-1"; if(needPortMapping != -1) { port = JedisUtil.hget("port", podName); if(port == null) { port = PortUtil.getPort() + ""; JedisUtil.hset("port", podName, port); } } // 构建任务字符串,便于redis存储 String task = buildParams.toJSONString(); // 此次请求只为轮询时间,只返回轮询的时间结果 if (times != 1) { double buildRank; try { buildRank = JedisUtil.zrank("task", task); } catch (Exception e) { response.put("ableToCreate", 0); response.put("waitNum", maxRunningPodNum + JedisUtil.zlen("task")); response.put("code", 0); response.put("msg", "等待评测"); return response; } if (buildRank != Double.MIN_VALUE) { logger.debug("任务:{} : 还在等待队列中!", task); response.put("ableToCreate", 0); response.put("waitNum", buildRank == Double.MAX_VALUE ? maxRunningPodNum + JedisUtil.zlen("task") : buildRank + maxRunningPodNum); response.put("code", 0); response.put("msg", "等待评测"); return response; } else { // 轮询时间请求,发现目标任务不在队列,就直接认为其正在运行 logger.debug("任务:{} : 正在执行中!", task); response.put("ableToCreate", 1); response.put("waitNum", 0); response.put("code", 0); response.put("port", Integer.parseInt(port)); response.put("msg", "正在评测"); return response; } } logger.info("kubernetes start#4**{}**** {}", tpiID, dateformat.format(System.currentTimeMillis())); // 获取Running状态的pod数 int runningPodNum = kubernetesService.getRunningPodNum(); // 判断是否可以立即执行(若pod已经存在,或是,pod不存在但是此时可以创建pod) boolean executeImmediately = kubernetesService.isPodRunning("tpiID", tpiID) || (runningPodNum < maxRunningPodNum && k8sService.ableToEvaluate()); if (executeImmediately) { // 直接执行任务 logger.info("execute start#5**{}**** {}", tpiID, dateformat.format(System.currentTimeMillis())); BuildThread buildThread = gameService.getBuildThread(buildParams); threadPoolTaskExecutor.execute(buildThread); long evaEnd = System.currentTimeMillis(); long costTime = evaEnd - evaStart; response.put("ableToCreate", 1); response.put("costTime", costTime); response.put("waitNum", 0); response.put("code", 0); response.put("port", port); response.put("msg", "评测完成"); logger.debug("直接执行任务!task: {}, tpi: {}", task, tpiID); } else { // 否则,将构建任务推入redis JedisUtil.zpush("task", task); //评测任务等待执行 response.put("ableToCreate", 0); //在等待队列中的位置 response.put("waitNum", JedisUtil.zlen("task") + maxRunningPodNum); response.put("code", 0); response.put("msg", "等待评测"); logger.debug("任务入队等待!task: {}, tpi: {}", task, tpiID); } return response; } /** * tpm版本库已更新,同步 * * @param tpiID * @param tpiGitURL * @param tpmGitURL * @return */ @RequestMapping(path = "/resetTpmRepository", method = RequestMethod.POST) @ApiOperation(value = "tpm版本库已更新,同步操作", httpMethod = "POST", produces = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public JSONObject reset( @ApiParam(name = "tpiID", required = true, value = "tpi") @RequestParam String tpiID, @ApiParam(name = "tpiGitURL", required = true, value = "学员对应当前实训的版本库地址,base64编码") @RequestParam String tpiGitURL, @ApiParam(name = "tpmGitURL", required = true, value = "学员对应当前实训的tpm版本库地址,base64编码") @RequestParam String tpmGitURL, @ApiParam(name = "identifier", required = true, value = "push权限") @RequestParam String identifier) throws Exception { logger.debug("tpm版本库已更新,同步tpi版本库,tpiID:{}", tpiID); JSONObject response = new JSONObject(); tpmGitURL = Base64Util.decode(tpmGitURL); tpiGitURL = Base64Util.decode(tpiGitURL); String tpiRepoName = StringHelper.getRepoName(tpiGitURL); String path = appConfig.getWorkspace() + File.separator + "myshixun_" + tpiID; try { // 从tpm远程库拉取代码到myshixun版本库然后强推到tpi远程库 gameService.gitPullFromTpm(path, tpmGitURL, tpiRepoName); gameService.gitPushToTpi(path, tpiGitURL, identifier); logger.debug("tpm库更新内容已同步,tpiID:{}", tpiID); response.put("code", 0); response.put("msg", "版本库更新成功"); } catch (GameException e) { logger.debug("tpm库更新内容同步失败, tpiID:{}", tpiID); response.put("code", -1); response.put("msg", "版本库同步失败!"); } return response; } /** * tpi版本库head是否存在,若缺失则修复 * * @param tpiID * @param tpiGitURL * @return * @throws Exception */ @RequestMapping(path = "/check") @ApiOperation(value = "进入实训时做校验", httpMethod = "POST", produces = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public JSONObject check( @ApiParam(name = "tpiID", required = true, value = "实训实例的ID") @RequestParam String tpiID, @ApiParam(name = "tpiGitURL", required = true, value = "学员对应当前实训的版本库地址,base64编码") @RequestParam String tpiGitURL) throws Exception { JSONObject response = new JSONObject(); tpiGitURL = Base64Util.decode(tpiGitURL); String tpiRepoName = StringHelper.getRepoName(tpiGitURL); String tpiRepoPath = appConfig.getWorkspace() + "/myshixun_" + tpiID + "/" + tpiRepoName; // 远程tpi库是否缺失head了 boolean noHead = ShellUtil.execute("cd " + tpiRepoPath + " && git remote show origin").contains("unknown"); if (noHead) { logger.debug("tpiID:{} 远程tpi版本库head丢失", tpiID); String identifier = StringHelper.getIdentifier(tpiGitURL); // 处理仓库head丢失 ssh -p1122 git@10.9.191.219 'cd /home/git/repositories/p79061248/klp26sqc.git/refs/tmp/; // cd `ls -t | sed -n "2p"`; cat head >../../heads/master; git update-ref HEAD `cat head`' String command = "ssh -p1122 git@" + appConfig.getGitIP() + " 'cd repositories/" + identifier + "/" + tpiRepoName + ".git/refs/tmp; cd `ls -t | sed -n \"2p\"`; cat head >../../heads/master; git update-ref HEAD `cat head`'"; JSONObject result = ShellUtil.executeAndGetExitStatus(command); response.put("fixHead", result.getIntValue("exitStatus")); } response.put("msg", "finished"); return response; } }