Запуск init-скрипта при старте task-а
This commit is contained in:
parent
ec3f3318ac
commit
05c04c49ed
|
|
@ -2,6 +2,7 @@ package ru.oa2.lti;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import ru.oa2.lti.infrastructure.runner.Runner;
|
||||||
import ru.oa2.lti.model.LtiLogin;
|
import ru.oa2.lti.model.LtiLogin;
|
||||||
import ru.oa2.lti.model.TaskData;
|
import ru.oa2.lti.model.TaskData;
|
||||||
import ru.oa2.lti.repository.LMSContentRepository;
|
import ru.oa2.lti.repository.LMSContentRepository;
|
||||||
|
|
@ -19,11 +20,14 @@ public class ApplicationService {
|
||||||
|
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final LMSContentRepository lmsContentRepository;
|
private final LMSContentRepository lmsContentRepository;
|
||||||
|
private final Runner runner;
|
||||||
|
|
||||||
public ApplicationService(JwtService jwtService,
|
public ApplicationService(JwtService jwtService,
|
||||||
LMSContentRepository lmsContentRepository) {
|
LMSContentRepository lmsContentRepository,
|
||||||
|
Runner runner) {
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
this.lmsContentRepository = lmsContentRepository;
|
this.lmsContentRepository = lmsContentRepository;
|
||||||
|
this.runner = runner;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Payload getPayload(LtiLogin ltiLogin) {
|
public Payload getPayload(LtiLogin ltiLogin) {
|
||||||
|
|
@ -41,7 +45,10 @@ public class ApplicationService {
|
||||||
//TODO добавить версию в Task и выбирать самую старшую и опубликованную
|
//TODO добавить версию в Task и выбирать самую старшую и опубликованную
|
||||||
TaskData data = tasks.stream().findFirst().get().getData();
|
TaskData data = tasks.stream().findFirst().get().getData();
|
||||||
|
|
||||||
//TODO запуск initScript
|
if (!runner.run(contextId, data.getInitScript())) {
|
||||||
|
return "Init script FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
return data.getDescription();
|
return data.getDescription();
|
||||||
} else {
|
} else {
|
||||||
return "Not Page";
|
return "Not Page";
|
||||||
|
|
@ -50,7 +57,6 @@ public class ApplicationService {
|
||||||
|
|
||||||
public String saveResult(String body) {
|
public String saveResult(String body) {
|
||||||
|
|
||||||
//TODO
|
|
||||||
return "{\n" +
|
return "{\n" +
|
||||||
" \"success\": true,\n" +
|
" \"success\": true,\n" +
|
||||||
" \"message\": \"Результат успешно получен и обработан\",\n" +
|
" \"message\": \"Результат успешно получен и обработан\",\n" +
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,28 @@
|
||||||
package ru.oa2.lti.controller;
|
package ru.oa2.lti.controller;
|
||||||
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
|
||||||
import ru.oa2.lti.ApplicationService;
|
import ru.oa2.lti.ApplicationService;
|
||||||
|
import ru.oa2.lti.infrastructure.LMSService;
|
||||||
import ru.oa2.lti.model.LtiLogin;
|
import ru.oa2.lti.model.LtiLogin;
|
||||||
import ru.oa2.lti.service.jwt.Payload;
|
import ru.oa2.lti.service.jwt.Payload;
|
||||||
import ru.oa2.lti.service.jwt.TokenService;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/tool/lti")
|
@RequestMapping("/tool/lti")
|
||||||
public class LoginController {
|
public class LaunchController {
|
||||||
|
|
||||||
private final ApplicationService service;
|
private final ApplicationService service;
|
||||||
private final TokenService tokenService;
|
private final LMSService lmsService;
|
||||||
|
|
||||||
public LoginController(ApplicationService applicationService,
|
public LaunchController(ApplicationService applicationService,
|
||||||
TokenService tokenService) {
|
LMSService lmsService) {
|
||||||
this.service = applicationService;
|
this.service = applicationService;
|
||||||
this.tokenService = tokenService;
|
this.lmsService = lmsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
|
|
@ -36,9 +33,7 @@ public class LoginController {
|
||||||
@RequestParam("lti_deployment_id") String ltiDeploymentId,
|
@RequestParam("lti_deployment_id") String ltiDeploymentId,
|
||||||
@RequestParam("lti_message_hint") String ltiMessageHint,
|
@RequestParam("lti_message_hint") String ltiMessageHint,
|
||||||
@RequestParam("target_link_uri") String targetLinkUri,
|
@RequestParam("target_link_uri") String targetLinkUri,
|
||||||
HttpServletRequest request,
|
HttpServletRequest request) {
|
||||||
RedirectAttributes redirectAttributes,
|
|
||||||
ServletResponse servletResponse) {
|
|
||||||
|
|
||||||
var ltiLogin = LtiLogin.builder()
|
var ltiLogin = LtiLogin.builder()
|
||||||
.clientId(clientId)
|
.clientId(clientId)
|
||||||
|
|
@ -62,8 +57,7 @@ public class LoginController {
|
||||||
session.setAttribute("payload", payload);
|
session.setAttribute("payload", payload);
|
||||||
|
|
||||||
if (payload.getContextId() != null) {
|
if (payload.getContextId() != null) {
|
||||||
var body = tokenService.ltiAuth(ltiMessageHint, iss, loginHint);
|
return lmsService.ltiAuth(ltiMessageHint, iss, loginHint);
|
||||||
return body.getBody();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "redirect:/tool/lti/select";
|
return "redirect:/tool/lti/select";
|
||||||
|
|
@ -85,8 +79,6 @@ public class LoginController {
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("description", data);
|
model.addAttribute("description", data);
|
||||||
model.addAttribute("codeTitle", "Dockerfile:");
|
|
||||||
model.addAttribute("inputCode", "FROM ...");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "task";
|
return "task";
|
||||||
|
|
@ -3,21 +3,17 @@ package ru.oa2.lti.controller;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.client.RestClient;
|
|
||||||
import ru.oa2.lti.ApplicationService;
|
import ru.oa2.lti.ApplicationService;
|
||||||
import ru.oa2.lti.model.LtiLogin;
|
import ru.oa2.lti.model.LtiLogin;
|
||||||
import ru.oa2.lti.service.jwt.JwtService;
|
import ru.oa2.lti.service.jwt.JwtService;
|
||||||
import ru.oa2.lti.service.jwt.TokenService;
|
import ru.oa2.lti.service.jwt.TokenService;
|
||||||
import ru.oa2.lti.service.results.ResultService;
|
import ru.oa2.lti.service.results.ResultService;
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/tool/lti/result")
|
@RequestMapping("/tool/lti/result")
|
||||||
|
|
@ -47,6 +43,7 @@ public class ResultController {
|
||||||
HttpSession session = req.getSession(false);
|
HttpSession session = req.getSession(false);
|
||||||
if (session == null) return ResponseEntity.status(401).build();
|
if (session == null) return ResponseEntity.status(401).build();
|
||||||
|
|
||||||
|
//TODO запуск скрипта проверки
|
||||||
var ltiLogin = (LtiLogin) session.getAttribute("lti_login");
|
var ltiLogin = (LtiLogin) session.getAttribute("lti_login");
|
||||||
var assessToken = tokenService.exchangeForAccessToken(
|
var assessToken = tokenService.exchangeForAccessToken(
|
||||||
ltiLogin.getClientId());
|
ltiLogin.getClientId());
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
|
|
||||||
```shell
|
|
||||||
curl -XPOST http://localhost:9999/tool/lti/login \
|
|
||||||
-d 'client_id=27e2d42d-d218-4ab9-b063-85e3ec87ec8f&iss=https%3A%2F%2Flocalhost&login_hint=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImU5ODA3YTg2LTVhNWQtNDI0MC1hYjI1LWNhYTUyZDliZjUxOSJ9.eyJkZXBsb3ltZW50S2V5IjoxLCJkZXBsb3ltZW50SWQiOiIyNzE1ZDA0NS03N2M1LTRkODgtYTFlNC05MmQ3MjFmY2M1NGIiLCJjb250ZXh0S2V5IjoxLCJjb250ZXh0SWQiOiJlMmE4ZGQxMy04MWJhLTQwMTEtODgwNi0wMjA5NGZkZjg5ZjMiLCJjb3Vyc2VhZG1pbiI6ZmFsc2UsImNvYWNoIjpmYWxzZSwicGFydGljaXBhbnQiOnRydWV9.vgPLHQA9scdED3_OwOy46h6VmzpN4arIHY3-YDBdhH4kuEqeCOjjtFbGdDqauKoQBTSVo4UvoK4JQLMhak6qsFchCj54mPob8jbaKLd0GnO_jY0sR609Nrk7Muq7cki_4PjVMX8TTHp-VYlSHjVxQH_z_D5Wld27J95z4qJjRU59GmvLGDqdLyerVVBO-zaavYsUbEEiAxoX3hmytxrarmJ7OHpxufNOeXzZ0DSGUmU5ycuTAqxODaHO1Y4rQM6XlvSfDh_TmXP8QEkatlp2cdjRpNWyOdUW_hZfbtkqukwt1ZP7KEgWzNI3vivpBjm2xfUG2YLwXPJqHa47NQgvsQ<i_deployment_id=2715d045-77c5-4d88-a1e4-92d721fcc54b<i_message_hint=1802240&target_link_uri=https%3A%2F%2Fopenolat.local%2Ftool'
|
|
||||||
```
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package ru.oa2.lti.infrastructure;
|
||||||
|
|
||||||
|
public interface LMSService {
|
||||||
|
|
||||||
|
String ltiAuth(String ltiLoginHint, String iss, String loginHint);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package ru.oa2.lti.infrastructure;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
import ru.oa2.lti.config.AppProperties;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class LMSServiceImpl implements LMSService {
|
||||||
|
|
||||||
|
private final RestClient restClient;
|
||||||
|
private final AppProperties appProperties;
|
||||||
|
|
||||||
|
public LMSServiceImpl(RestClient restClient, AppProperties appProperties) {
|
||||||
|
this.restClient = restClient;
|
||||||
|
this.appProperties = appProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String ltiAuth(String ltiLoginHint, String iss, String loginHint) {
|
||||||
|
var result = restClient
|
||||||
|
.get()
|
||||||
|
.uri("/lti/auth?" +
|
||||||
|
"state=start" +
|
||||||
|
"&code=code1" +
|
||||||
|
"&iss=" + iss +
|
||||||
|
"&login_hint=" + loginHint +
|
||||||
|
"&redirect_uri=" + appProperties.lmsUrl() + "/tool/lti/redirect" +
|
||||||
|
"<i_message_hint=" + ltiLoginHint)
|
||||||
|
.retrieve();
|
||||||
|
|
||||||
|
ResponseEntity<String> body = result.toEntity(String.class);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("lti/auth RESPONSE: {}", body);
|
||||||
|
}
|
||||||
|
return body.getBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ru.oa2.lti.infrastructure.runner;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface Runner {
|
||||||
|
|
||||||
|
boolean run(UUID userId, String script);
|
||||||
|
|
||||||
|
boolean check(UUID userId, String script);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package ru.oa2.lti.infrastructure.runner;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RunnerImpl implements Runner {
|
||||||
|
|
||||||
|
private final String TEMP_DIR = "./temp/";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean run(UUID userId, String script) {
|
||||||
|
try {
|
||||||
|
return runScript(userId, script);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error(ex.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO добавить вывод результата/интерпретации
|
||||||
|
@Override
|
||||||
|
public boolean check(UUID userId, String script) {
|
||||||
|
try {
|
||||||
|
return runScript(userId, script);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error(ex.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean runScript(UUID userId, String script) throws Exception {
|
||||||
|
|
||||||
|
var processBuilder = new ProcessBuilder();
|
||||||
|
processBuilder.command("bash", saveFile(userId, script));
|
||||||
|
|
||||||
|
var process = processBuilder.start();
|
||||||
|
|
||||||
|
BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(process.getInputStream())
|
||||||
|
);
|
||||||
|
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
output.append(line).append("\n");
|
||||||
|
}
|
||||||
|
log.info(output.toString());
|
||||||
|
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
return exitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String saveFile(UUID userId, String script) throws IOException {
|
||||||
|
Path path = Paths.get(TEMP_DIR + userId.toString());
|
||||||
|
|
||||||
|
if (!Files.exists(path)) {
|
||||||
|
Files.createDirectories(path);
|
||||||
|
}
|
||||||
|
var tm = System.currentTimeMillis();
|
||||||
|
var scriptPath = Files.write(
|
||||||
|
Paths.get(TEMP_DIR + userId + "/" + tm),
|
||||||
|
script.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
return scriptPath.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,11 +16,9 @@ import java.util.Map;
|
||||||
public class TokenService {
|
public class TokenService {
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
private final RestClient restClient;
|
|
||||||
private final AppProperties appProperties;
|
private final AppProperties appProperties;
|
||||||
|
|
||||||
public TokenService(RestClient restClient, AppProperties appProperties) {
|
public TokenService(AppProperties appProperties) {
|
||||||
this.restClient = restClient;
|
|
||||||
this.appProperties = appProperties;
|
this.appProperties = appProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,24 +63,4 @@ public class TokenService {
|
||||||
throw new RuntimeException("Не удалось получить access token", e);
|
throw new RuntimeException("Не удалось получить access token", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<String> ltiAuth(String ltiLoginHint, String iss, String loginHint) {
|
|
||||||
var result = restClient
|
|
||||||
.get()
|
|
||||||
.uri("/lti/auth?" +
|
|
||||||
"state=start" +
|
|
||||||
"&code=code1" +
|
|
||||||
"&iss=" + iss +
|
|
||||||
"&login_hint=" + loginHint +
|
|
||||||
"&redirect_uri=" + appProperties.lmsUrl() + "/tool/lti/redirect" +
|
|
||||||
"<i_message_hint=" + ltiLoginHint)
|
|
||||||
.retrieve();
|
|
||||||
|
|
||||||
ResponseEntity<String> body = result.toEntity(String.class);
|
|
||||||
if (log.isDebugEnabled()) {
|
|
||||||
log.debug("lti/auth RESPONSE: {}", body);
|
|
||||||
}
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package ru.oa2.lti.service.results.dto;
|
||||||
|
|
||||||
|
public record ResultResponse(
|
||||||
|
String status
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
document.getElementById('submitBtn').addEventListener('click', function () {
|
||||||
|
// Показываем, что загрузка началась
|
||||||
|
const statusContainer = document.getElementById('statusContainer');
|
||||||
|
const icon = document.getElementById('icon');
|
||||||
|
const statusLabel = document.getElementById('statusLabel');
|
||||||
|
|
||||||
|
// Получаем данные пользователя
|
||||||
|
const contextId = document.getElementById('contextId')?.value || 'unknown';
|
||||||
|
const participantId = document.getElementById('participantId')?.value || 'unknown';
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
contextId: contextId,
|
||||||
|
participantId: participantId
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/tool/lti/result/docker', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error(`Ошибка сети: ${response.status}`);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Показываем статус
|
||||||
|
statusContainer.style.display = 'flex';
|
||||||
|
|
||||||
|
if (data.status === 'success') {
|
||||||
|
icon.className = 'fa-solid fa-check-circle status-success';
|
||||||
|
statusLabel.textContent = 'Success';
|
||||||
|
statusLabel.className = 'status-label status-success';
|
||||||
|
} else {
|
||||||
|
icon.className = 'fa-solid fa-xmark-circle status-failed';
|
||||||
|
statusLabel.textContent = 'Failed';
|
||||||
|
statusLabel.className = 'status-label status-failed';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
statusContainer.style.display = 'flex';
|
||||||
|
icon.className = 'fa-solid fa-xmark-circle status-failed';
|
||||||
|
statusLabel.textContent = 'Failed';
|
||||||
|
statusLabel.className = 'status-label status-failed';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,182 +4,116 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body, html {
|
body, html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
height: 90vh;
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
.row {
|
.content-section {
|
||||||
height: 100%;
|
flex: 1;
|
||||||
}
|
padding: 30px 20px;
|
||||||
.left-section {
|
|
||||||
border-right: 2px solid #dee2e6;
|
|
||||||
padding: 20px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
.right-section {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.right-top {
|
|
||||||
height: 50%;
|
|
||||||
padding: 20px;
|
|
||||||
border-bottom: 2px solid #dee2e6;
|
|
||||||
}
|
|
||||||
.right-bottom {
|
|
||||||
height: 50%;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.task-block {
|
.task-block {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 20px;
|
padding: 30px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||||
margin-bottom: 20px;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
.code-block {
|
.footer {
|
||||||
background: #f5f5f5;
|
padding: 15px 20px;
|
||||||
padding: 15px;
|
background-color: #f1f3f5;
|
||||||
border-radius: 5px;
|
border-top: 1px solid #dee2e6;
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
.input-output {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.status-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
.status-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.status-success {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
.status-failed {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
.status-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.submit-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
resize: none;
|
min-height: 200px;
|
||||||
font-family: 'Courier New', monospace;
|
padding: 12px;
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.output-area {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
.run-button {
|
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
padding: 10px 30px;
|
border: 1px solid #ced4da;
|
||||||
font-size: 16px;
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: none;
|
||||||
}
|
}
|
||||||
.result-title {
|
textarea:focus {
|
||||||
margin-bottom: 10px;
|
outline: none;
|
||||||
color: #495057;
|
border-color: #80bdff;
|
||||||
font-weight: bold;
|
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
<!-- Левая секция -->
|
<!-- Описание задачи — на всю ширину -->
|
||||||
<div class="col-md-6 left-section">
|
<div class="content-section">
|
||||||
<h3>Условие задачи</h3>
|
<div class="task-block" th:utext="${description}">
|
||||||
<div class="task-block" th:utext="${description}">
|
<!-- Сюда вставляется HTML-описание задачи -->
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Правая секция -->
|
|
||||||
<div class="col-md-6 right-section">
|
|
||||||
<!-- Верхняя часть правой секции -->
|
|
||||||
<div class="right-top">
|
|
||||||
<h4>Введите решение</h4>
|
|
||||||
<div class="input-output">
|
|
||||||
<div class="code-prompt mb-2" th:utext="${codeTitle}">
|
|
||||||
<!-- Текст подсказки для ввода кода -->
|
|
||||||
</div>
|
|
||||||
<textarea id="codeInput" class="form-control" th:text="${inputCode}">
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Нижняя часть правой секции -->
|
|
||||||
<div class="right-bottom">
|
|
||||||
<h4>Результат выполнения</h4>
|
|
||||||
<div class="input-output">
|
|
||||||
<div class="result-title">Вывод программы:</div>
|
|
||||||
<div id="outputArea" class="output-area">
|
|
||||||
<!-- Здесь будет появляться результат -->
|
|
||||||
Результат выполнения появится здесь...
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<button id="runButton" class="btn btn-primary run-button">
|
|
||||||
<i class="fas fa-play"></i> Запустить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Футер: статус + кнопка -->
|
||||||
|
<div class="footer">
|
||||||
|
<!-- Статус (иконка + надпись) -->
|
||||||
|
<div id="statusContainer" class="status-container" style="display: none;">
|
||||||
|
<div id="statusIcon" class="status-icon">
|
||||||
|
<i id="icon" class="fa-solid"></i>
|
||||||
|
</div>
|
||||||
|
<div id="statusLabel" class="status-label"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка отправки -->
|
||||||
|
<button id="submitBtn" class="btn btn-primary submit-btn">
|
||||||
|
<i class="fas fa-paper-plane"></i> Отправить на проверку
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- Скрытые поля (если нужно передать контекст) -->
|
||||||
document.getElementById('runButton').addEventListener('click', function() {
|
<input type="hidden" id="contextId" th:value="${contextId}" />
|
||||||
const codeInput = document.getElementById('codeInput').value;
|
<input type="hidden" id="participantId" th:value="${participantId}" />
|
||||||
const outputArea = document.getElementById('outputArea');
|
|
||||||
|
|
||||||
// Показываем индикатор загрузки
|
<script th:src="@{/tool/lti/js/main.js}"></script>
|
||||||
outputArea.innerHTML = '<div class="text-center"><i class="fas fa-spinner fa-spin"></i> Отправка данных...</div>';
|
|
||||||
|
|
||||||
// Получаем данные пользователя и контекста
|
|
||||||
// Предположим, что эти значения хранятся в data-атрибутах или скрытых полях
|
|
||||||
const contextId = document.getElementById('contextId')?.value || 'unknown_context';
|
|
||||||
const participantId = document.getElementById('participantId')?.value || 'unknown_participant';
|
|
||||||
|
|
||||||
// Формируем JSON-объект для отправки
|
|
||||||
const payload = {
|
|
||||||
contextId: contextId,
|
|
||||||
participantId: participantId,
|
|
||||||
text: codeInput
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/tool/lti/result/docker', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Ошибка сети: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
// Успешный ответ от сервера
|
|
||||||
outputArea.innerHTML = '<div class="text-success">✓ Данные успешно отправлены!</div>';
|
|
||||||
console.log('Ответ сервера:', data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
// Обработка ошибок сети или сервера
|
|
||||||
outputArea.innerHTML = `
|
|
||||||
<div class="text-danger">
|
|
||||||
<strong>Ошибка отправки:</strong><br>
|
|
||||||
${error.message}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
console.error('Ошибка при отправке:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Автоматический рост textarea при вводе
|
|
||||||
document.getElementById('codeInput').addEventListener('input', function() {
|
|
||||||
this.style.height = 'auto';
|
|
||||||
this.style.height = (this.scrollHeight) + 'px';
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package ru.oa2.lti.infrastructure;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import ru.oa2.lti.infrastructure.runner.Runner;
|
||||||
|
import ru.oa2.lti.infrastructure.runner.RunnerImpl;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
|
||||||
|
|
||||||
|
@SpringBootTest(classes = {RunnerImpl.class})
|
||||||
|
public class RunnerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Runner runner;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void runnerTest() {
|
||||||
|
|
||||||
|
var script = "#!/bin/bash\n" +
|
||||||
|
"echo \"Запуск развёртывания...\"\n" +
|
||||||
|
"sleep 2\n" +
|
||||||
|
"echo \"Развёртывание завершено.\"";
|
||||||
|
|
||||||
|
var result = runner.run(UUID.randomUUID(), script);
|
||||||
|
|
||||||
|
assertThat(result).isEqualTo(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue