/* * Compiler.java * * Copyright (C) 2015 Pixelgaffer * * This work is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation; either version 2 of the License, or any later * version. * * This work is distributed in the hope that it will be useful, but without * any warranty; without even the implied warranty of merchantability or * fitness for a particular purpose. See version 2 and version 3 of the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.pixelgaffer.turnierserver.compile; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import org.pixelgaffer.turnierserver.networking.DatastoreFtpClient; import org.pixelgaffer.turnierserver.networking.bwprotocol.WorkerCommandAnswer; import org.pixelgaffer.turnierserver.networking.messages.WorkerCommand; import it.sauronsoftware.ftp4j.FTPAbortedException; import it.sauronsoftware.ftp4j.FTPDataTransferException; import it.sauronsoftware.ftp4j.FTPException; import it.sauronsoftware.ftp4j.FTPIllegalReplyException; import it.sauronsoftware.ftp4j.FTPListParseException; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.ToString; /** * Diese Klasse ist eine abstrakte Implementation eines Compilers, der die * Verbindung zum FTP-Server größtenteils übernimmt. */ public abstract class Compiler { public Compiler (int ai, int version, int game) { super(); this.ai = ai; this.version = version; this.game = game; } public static Compiler getCompiler (String language) throws ReflectiveOperationException { return getCompiler(-1, -1, -1, language); } public static Compiler getCompiler (int ai, int version, int game, String language) throws ReflectiveOperationException { Class<?> clazz = Class.forName("org.pixelgaffer.turnierserver.compile." + language + "Compiler"); Compiler c = (Compiler)clazz .getConstructor(Integer.TYPE, Integer.TYPE, Integer.TYPE) .newInstance(ai, version, game); return c; } /** * Diese Klasse wird verwendet, um Ausgaben beim Kompilieren einer KI an * Frontend und FTP weiterzuleiten. */ @RequiredArgsConstructor private class CompilerDebugWriter extends Writer { @NonNull private Writer ftpFile; @NonNull private Backend backend; private String buf = ""; @Override public void close () throws IOException { flush(); ftpFile.close(); } @Override public void flush () throws IOException { ftpFile.flush(); if (getUuid() != null) { backend.sendAnswer(new WorkerCommandAnswer(WorkerCommand.COMPILE, WorkerCommandAnswer.MESSAGE, getUuid(), buf)); } buf = ""; } @Override public void write (char[] buf, int off, int len) throws IOException { write(new String(buf, off, len)); } @Override public void write (@NonNull String s) throws IOException { ftpFile.write(s); buf += s; } } @Getter private int ai; @Getter private int version; @Getter private int game; @Getter @Setter private UUID uuid; @Getter @Setter(AccessLevel.PROTECTED) private String command; @Getter @Setter(AccessLevel.PROTECTED) private String arguments[]; @Getter private final Map<String, String> environment = new HashMap<>(); public abstract String getLanguage(); @AllArgsConstructor @EqualsAndHashCode @ToString protected class RequiredLibrary { public String name; public String path; } @Getter(AccessLevel.PROTECTED) private final Set<RequiredLibrary> libs = new HashSet<>(); public CompileResult compileAndUpload (@NonNull Backend backend, LibraryDownloader libs) throws IOException, InterruptedException, FTPIllegalReplyException, FTPException, FTPDataTransferException, FTPAbortedException, FTPListParseException { // source runterladen if (backend != null) backend.sendAnswer(new WorkerCommandAnswer(WorkerCommand.COMPILE, WorkerCommandAnswer.MESSAGE, getUuid(), "> Lade Quelltext herunter ...\n")); File srcdir = DatastoreFtpClient.retrieveAiSource(getAi(), getVersion()); // zeugs anlegen File bindir = Files.createTempDirectory("aibin").toFile(); File output = Files.createTempFile("compiler", ".txt").toFile(); FileWriter ftpFile = new FileWriter(output); Writer w = new CompilerDebugWriter(ftpFile, backend); PrintWriter pw = new PrintWriter(w, true); // properties lesen Properties p = new Properties(); try { p.load(new FileInputStream(new File(srcdir, "settings.prop"))); } catch (IOException ioe) { pw.println("> Fehler beim Lesen der Datei settings.prop: " + ioe); pw.close(); delete(srcdir); delete(bindir); return new CompileResult(false, output); } // compilieren boolean success = compile(srcdir, bindir, p, pw, libs); // aufräumen delete(srcdir); if (success) { // die Properties-Datei für die Sandbox schreiben writeStartProps(bindir); // packen File archive = Files.createTempFile("aibin", ".tar.bz2").toFile(); List<String> ignored = getLibs().stream().map( (lib) -> lib.path).collect(Collectors.toList()); String files[] = bindir.list( (dir, name) -> !name.equals("libraries.txt") && !name.equals("settings.prop") && !ignored.contains(name)); String cmd[] = new String[files.length + 3]; cmd[0] = "tar"; cmd[1] = "cfj"; cmd[2] = archive.getAbsolutePath(); System.arraycopy(files, 0, cmd, 3, files.length); System.out.println(execute(bindir, pw, cmd)); // hochladen if (backend != null) backend.sendAnswer(new WorkerCommandAnswer(WorkerCommand.COMPILE, WorkerCommandAnswer.MESSAGE, getUuid(), "> Lade kompilierte KI hoch ...\n")); DatastoreFtpClient.storeAi(getAi(), getVersion(), new FileInputStream(archive)); // aufräumen delete(archive); } // aufräumen delete(bindir); pw.close(); return new CompileResult(success, output); } /** * Diese Methode kompiliert den Quelltext einer KI aus srcdir nach bindir. * In der Datei properties stehen die zur KI gehörenden Eigenschaften wie * z.B. die Main-Klasse in Java. */ public String compile (File srcdir, File bindir, File properties, LibraryDownloader libs) throws IOException, InterruptedException, CompileFailureException { if (!bindir.exists() && !bindir.mkdirs()) throw new CompileFailureException("Konnte das Verzeichnis " + bindir + " nicht anlegen!"); // den output in einen String ausgeben StringWriter sw = new StringWriter(); PrintWriter output = new PrintWriter(sw); // die properties laden Properties p = new Properties(); p.load(new FileReader(properties)); // kompilieren boolean success = compile(srcdir, bindir, p, output, libs); // die Properties-Datei für die Sandbox schreiben writeStartProps(bindir); output.flush(); if (success) return sw.toString(); else throw new CompileFailureException(sw.toString()); } public abstract boolean compile (File srcdir, File bindir, Properties p, PrintWriter output, LibraryDownloader libs) throws IOException, InterruptedException; private void writeStartProps (File bindir) throws IOException { Properties props = new Properties(); props.put("language", getLanguage()); props.put("command", getCommand()); props.put("arguments.size", Integer.toString(getArguments().length)); for (int i = 0; i < getArguments().length; i++) props.put("arguments." + i, getArguments()[i]); props.put("libraries.size", Integer.toString(getLibs().size())); int count = 0; for (RequiredLibrary lib : getLibs()) { props.put("libraries." + count + ".name", lib.name); props.put("libraries." + count + ".path", lib.path); count++; } props.put("environment.size", Integer.toString(getEnvironment().size())); count = 0; for (String key : getEnvironment().keySet()) { props.put("environment." + count + ".key", key); props.put("environment." + count + ".value", getEnvironment().get(key)); count++; } props.store(new FileOutputStream(new File(bindir, "start.prop")), "GENERATED FILE - DO NOT EDIT"); } protected String relativePath (File absolute, File base) { Path absolutePath = absolute.toPath(); Path basePath = base.toPath(); Path relative = basePath.relativize(absolutePath); return relative.toString(); } protected void copy (File in, File out) throws IOException { out.mkdirs(); out.delete(); FileInputStream fis = new FileInputStream(in); FileOutputStream fos = new FileOutputStream(out); byte buf[] = new byte[8192]; int read; while ((read = fis.read(buf)) > 0) fos.write(buf, 0, read); fis.close(); fos.close(); } protected void delete (File f) { if (f.isDirectory()) { for (File f0 : f.listFiles()) delete(f0); } f.delete(); } protected int execute (File wd, PrintWriter output, Map<String, String> env, List<String> command) throws IOException, InterruptedException { output.print("$"); for (String cmd : command) { if (cmd.contains(" ")) cmd = "\"" + cmd + "\""; output.print(" "); output.print(cmd); } output.println(); ProcessBuilder pb = new ProcessBuilder(command); pb.environment().putAll(env); File log = Files.createTempFile("compiler", ".txt").toFile(); pb.redirectErrorStream(true); pb.redirectOutput(log); if (wd != null) pb.directory(wd); Process p = pb.start(); int returncode = p.waitFor(); Reader in = new FileReader(log); char buf[] = new char[8192]; int read; while ((read = in.read(buf)) > 0) output.write(buf, 0, read); in.close(); log.delete(); output.flush(); return returncode; } protected int execute (File wd, PrintWriter output, Map<String, String> env, String ... command) throws IOException, InterruptedException { return execute(wd, output, env, Arrays.asList(command)); } protected int execute (File wd, PrintWriter output, List<String> command) throws IOException, InterruptedException { return execute(wd, output, new HashMap<String, String>(), command); } protected int execute (File wd, PrintWriter output, String ... command) throws IOException, InterruptedException { return execute(wd, output, new HashMap<String, String>(), Arrays.asList(command)); } }