/* * Copyright 2015-2016 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE 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 3 of the License, or (at your option) any * later version. * * The CCRE 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 the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.deployment; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.file.Files; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.jar.Manifest; import ccre.frc.FRCApplication; import ccre.log.Logger; /** * A collection of utilities for building and downloading code for the roboRIO. * An instance of this class represents a specific discovered roboRIO on the * network. * * @author skeggsc */ public class DepRoboRIO { /** * The expected image number for the roboRIO to have. */ public static final int EXPECTED_IMAGE = 2016019; /** * Specifies that the THIN version of the roboRIO libraries should be used, * which does not include the DeploymentEngine or Emulator. */ public static final boolean LIBS_THIN = false; /** * Specifies that the THICK version of the roboRIO libraries should be used, * which includes the DeploymentEngine and Emulator. */ public static final boolean LIBS_THICK = true; private static final Random random = new Random(); /** * A SSH connection to the roboRIO as a specific account. This connection * can be used to interact with the roboRIO in various ways. * * @author skeggsc */ public class RIOShell extends Shell { private RIOShell(InetAddress ip, String username, String password) throws IOException { super(ip, username, password); } /** * Check to see if the JRE is currently installed on the roboRIO. * * @return true if the JRE is installed, or false otherwise. * @throws IOException if the connection fails. */ public boolean checkJRE() throws IOException { return exec("test -d /usr/local/frc/JRE") == 0; } /** * If there are any logfiles on the robot, packages them up and stick * the tar.gz package them into the specified directory. * * @param destdir the directory to archive logfiles into. * @throws IOException if the connection fails, or the archive cannot be * written out. */ public void archiveLogsTo(File destdir) throws IOException { if (this.exec("ls ccre-storage/log-* >/dev/null 2>/dev/null") == 0) { long name = random.nextLong(); this.execCheck("tar -czf logs-" + name + ".tgz ccre-storage/log-*"); this.execCheck("mkdir /tmp/logs-" + name + "/ && mv ccre-storage/log-* /tmp/logs-" + name + "/"); Files.copy(this.receiveFile("logs-" + name + ".tgz"), new File(destdir, "logs-" + name + ".tgz").toPath()); this.execCheck("rm logs-" + name + ".tgz"); } } /** * Verifies that the roboRIO is set up with the * {@link DepRoboRIO#EXPECTED_IMAGE} and an installed JRE. An * explanatory exception is thrown if not. * * @throws IOException if something fails during attempts to verify. */ public void verifyRIO() throws IOException { verifyRIO(EXPECTED_IMAGE); } /** * Verifies that the roboRIO is set up with <code>expected_image</code> * as the image and a properly installed JRE. An explanatory exception * is thrown if not. * * @param expected_image the image number that is expected to be found. * If it is less than 1000, 2015000 is added to match the new image * number versioning scheme. * @throws IOException if something fails during attempts to verify. */ public void verifyRIO(int expected_image) throws IOException { if (expected_image < 1000) { expected_image += 2015000; } int image = getRIOImageAndYear(); if (image != expected_image) { throw new RuntimeException("Unsupported roboRIO image number! You need to have " + expected_image + " instead of " + image); } if (!checkJRE()) { throw new RuntimeException("JRE not installed! See https://wpilib.screenstepslive.com/s/4485/m/13503/l/288822-installing-java-8-on-the-roborio-using-the-frc-roborio-java-installer-java-only"); } } /** * Downloads the specified Jar as a program to the roboRIO, along with * the necessary supporting scripts. A RIOShell for a connection with * administrator access is required. * * @param jar the Jar to download. * @param adminshell a RIOShell with administrator access. * @throws IOException if something fails during download. */ public void downloadCode(File jar, RIOShell adminshell) throws IOException { Logger.info("Starting deployment..."); sendFileTo(jar, "/home/lvuser/FRCUserProgram.jar"); Logger.info("Primary deployment complete."); // prevent any text-busy issues adminshell.execCheck("rm -f /usr/local/frc/bin/netconsole-host"); adminshell.sendBinResourceTo(DepRoboRIO.class, "/edu/wpi/first/wpilibj/binaries/netconsole-host", "/usr/local/frc/bin/", 0755); sendTextResourceTo(DepRoboRIO.class, "/edu/wpi/first/wpilibj/binaries/robotCommand", "/home/lvuser/", 0755); Logger.info("Download complete."); } /** * Attempts to stop any running robot code. If the code cannot be * stopped, or was not running, it doesn't report any errors, as this * doesn't usually end up being a problem. * * @throws IOException if the connection fails. */ public void stopRobot() throws IOException { // it's okay if this fails exec("killall netconsole-host"); } /** * Starts the currently-loaded robot code, including stopping any * currently-running robot code. * * @throws IOException if the connection or the attempt fails. */ public void startRobot() throws IOException { execCheck(". /etc/profile.d/natinst-path.sh; /usr/local/frc/bin/frcKillRobot.sh -t -r"); } /** * Downloads the Jar file <code>code</code> to the robot, and restarts * the robot code. * * @param code the Jar file to download. * @throws IOException if something fails. */ public void downloadAndStart(File code) throws IOException { try (DepRoboRIO.RIOShell ashell = openAdminShell()) { ashell.stopRobot(); downloadCode(code, ashell); } startRobot(); } /** * Downloads the Artifact <code>result</code> to the robot, once * converted to a Jar, and restarts the robot code. * * @param result the Artifact to download. * @throws IOException if something fails. */ public void downloadAndStart(Artifact result) throws IOException { downloadAndStart(result.toJar(false).toFile()); } } private static final String VERSION_BEGIN = "FRC_roboRIO_"; private static final String DEFAULT_USERNAME = "lvuser"; private static final String DEFAULT_PASSWORD = ""; private static final String DEFAULT_ADMIN_USERNAME = "admin"; private static final String DEFAULT_ADMIN_PASSWORD = ""; /** * Finds the path to the roboRIO compiled Jar file, either the thick or thin * version depending on whether {@link #LIBS_THICK} or {@link #LIBS_THIN} is * specified. * * The difference between the two is that LIBS_THICK also includes the * Deployment Engine and Emulator. * * @param thick if the thick version should be used. * @return the discovered Jar file. */ public static File getJarFile(boolean thick) { File out = new File(DepProject.ccreProject("roboRIO"), thick ? "roboRIO.jar" : "roboRIO-lite.jar"); if (!out.exists() || !out.isFile()) { throw new RuntimeException("roboRIO Jar cannot be found!"); } return out; } /** * Provides the roboRIO compiled Jar as a {@link Jar}, either the thick or * thin version depending on whether {@link #LIBS_THICK} or * {@link #LIBS_THIN} is specified. * * The difference between the two is that LIBS_THICK also includes the * Deployment Engine and Emulator. * * @param thick if the thick version should be used. * @return the Jar artifact. * @throws IOException if the Jar is not properly found */ public static Jar getJar(boolean thick) throws IOException { return new Jar(getJarFile(thick)); } /** * Generates the correct Manifest for a roboRIO application that has the * specified CCRE main class. * * The main class must implement {@link ccre.frc.FRCApplication} * * @param main the main class in dot form, for example * <code>org.team1540.example.Example</code>. * @return the generated Manifest. */ public static Manifest manifest(String main) { return DepJar.manifest("Main-Class", "ccre.frc.DirectFRCImplementation", "CCRE-Main", main, "Class-Path", "."); } /** * Generates the correct Manifest for a roboRIO application that has the * specified CCRE main class. The class must implement * {@link ccre.frc.FRCApplication}. * * @param main the main class as a Class object. * @return the generated Manifest. */ public static Manifest manifest(Class<? extends FRCApplication> main) { // repeated FRCApplication check just to avoid getting around it at // runtime. return manifest(main.asSubclass(FRCApplication.class).getName()); } /** * Discovers a roboRIO on the network for the specified team number, and * then verifies that it is set up properly. * * This discovers a roboRIO in the same way as {@link #discover(int)}, and * then verifies it in the same way as {@link RIOShell#verifyRIO()}. * * @param team_number the team number, used for determining where to look * for roboRIOs. * @return a {@link RIOShell} providing access to the roboRIO. * @throws IOException if something fails during these steps. */ public static RIOShell discoverAndVerify(int team_number) throws IOException { DepRoboRIO rio = discover(team_number); RIOShell shell = rio.openDefaultShell(); try { shell.verifyRIO(); return shell; } catch (Throwable thr) { try { shell.close(); } catch (IOException ex) { thr.addSuppressed(ex); } throw thr; } } /** * Discovers a roboRIO on the network for the specified team number. This * tries mDNS, USB, and the fallback 10.XX.YY.2 address. * * @param team_number the team number to use to calculate mDNS names and * fallback addresses. * @return the discovered DepRoboRIO instance that represents the discovered * remote roboRIO. * @throws UnknownHostException if a roboRIO cannot be found. */ public static DepRoboRIO discover(int team_number) throws UnknownHostException { DepRoboRIO rio = byNameOrIP("roboRIO-" + team_number + "-FRC.local"); if (rio == null) { rio = byNameOrIP("172.22.11.2"); } if (rio == null) { rio = byNameOrIP("10." + (team_number / 100) + "." + (team_number % 100) + ".2"); } if (rio == null) { // 2015 mDNS name format rio = byNameOrIP("roboRIO-" + team_number + ".local"); } if (rio == null) { throw new UnknownHostException("Cannot reach roboRIO over mDNS, ethernet-over-USB, or via static 10." + (team_number / 100) + "." + (team_number % 100) + ".2 address."); } return rio; } /** * Attempts to find a roboRIO on the network at <code>ip</code>. * * @param ip the IP address or hostname to try to connect to. * @return the discovered DepRoboRIO instance that represents the discovered * remote roboRIO, or null if it isn't found. */ public static DepRoboRIO byNameOrIP(String ip) { InetAddress inaddr; try { inaddr = InetAddress.getByName(ip); try (Socket sock = new Socket()) { sock.connect(new InetSocketAddress(inaddr, 22), 500); } } catch (IOException e) { return null; } return new DepRoboRIO(inaddr); } private final InetAddress ip; private DepRoboRIO(InetAddress ip) { this.ip = ip; } /** * Connects to this roboRIO with a username and password. * * @param username the username for the user, often <code>lvuser</code> or * <code>admin</code>. * @param password the password to use to connect, often the empty string. * @return the newly-opened connection. * @throws IOException if the connection cannot be established. */ public RIOShell openShell(String username, String password) throws IOException { return new RIOShell(ip, username, password); } /** * Connects to this roboRIO with the default username and password for the * main user account. * * @return the newly-opened connection. * @throws IOException if the connection cannot be established. */ public RIOShell openDefaultShell() throws IOException { return openShell(DEFAULT_USERNAME, DEFAULT_PASSWORD); } /** * Connects to this roboRIO with the default username and password for the * administrator user account. * * @return the newly-opened connection. * @throws IOException if the connection cannot be established. */ public RIOShell openAdminShell() throws IOException { return openShell(DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD); } /** * Determines the image version installed on this roboRIO, excluding the * year. * * @return the image number. For example, 23 for * <code>FRC_roboRIO_2015_v23</code>. * @throws IOException if the roboRIO's responses do not match the format * expectations. * @deprecated the year is very important! do not use this otherwise. */ @Deprecated public int getRIOImage() throws IOException { return getRIOImageAndYear() % 1000; } /** * Determines the image version installed on this roboRIO, including the * year. * * @return the image number. For example, 2015023 for * <code>FRC_roboRIO_2015_v23</code>, or 2016019 for * <code>FRC_roboRIO_2016_v19</code>. * @throws IOException if the roboRIO's responses do not match the format * expectations. */ public int getRIOImageAndYear() throws IOException { URLConnection connection = new URL("http://" + ip.getHostAddress() + "/nisysapi/server").openConnection(); connection.setDoInput(true); connection.setDoOutput(true); connection.setUseCaches(false); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { StringBuilder content = new StringBuilder(); HashMap<String, String> map = new HashMap<>(); map.put("Function", "GetPropertiesOfItem"); map.put("Plugins", "nisyscfg"); map.put("Items", "system"); Iterator<Entry<String, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); content.append(URLEncoder.encode(entry.getKey(), "UTF-16LE")).append('=').append(URLEncoder.encode(entry.getValue(), "UTF-16LE")); if (iterator.hasNext()) { content.append('&'); } } outputStream.writeBytes(content.toString()); outputStream.flush(); } StringBuilder file = new StringBuilder(); try (BufferedReader rin = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-16LE"))) { String line; while ((line = rin.readLine()) != null) { file.append(line); } } String contents = file.toString(); if (!contents.contains(VERSION_BEGIN)) { throw new IOException("Cannot find roboRIO image version response!"); } int index = contents.indexOf(VERSION_BEGIN) + VERSION_BEGIN.length(); int end = index; while (Character.isDigit(contents.charAt(end))) { end++; } int year; try { year = Integer.parseInt(contents.substring(index, end)); } catch (NumberFormatException ex) { throw new IOException("Could not parse roboRIO image version!", ex); } if (contents.charAt(end) != '_' || contents.charAt(end + 1) != 'v') { throw new IOException("Cannot find valid roboRIO image version response!"); } index = end += 2; while (Character.isDigit(contents.charAt(end))) { end++; } int rev; try { rev = Integer.parseInt(contents.substring(index, end)); } catch (NumberFormatException ex) { throw new IOException("Could not parse roboRIO image version!", ex); } return year * 1000 + rev; // for example, 2015_v19 becomes 2015019 } /** * Builds a directory of source files for a project against the roboRIO * support classes, and combines it with the roboRIO support libraries. * * @param source the directory of source files. * @param main the main class of the application. * @return the resulting Artifact of everything combined. * @throws IOException if the build or combination fails. */ public static Artifact build(File source, Class<? extends FRCApplication> main) throws IOException { // we need to compile against all the libraries because, if we don't, // the Deployment class won't build. // TODO: could there be a better solution for this? Artifact newcode = DepJava.build(source, DepRoboRIO.getJarFile(LIBS_THICK)); // TODO: re-enable this verifier // try (Jar jar = DepRoboRIO.getJar(LIBS_THICK)) { // PhaseVerifier.verify(newcode, jar); // } return DepJar.combine(DepRoboRIO.manifest(main), JarBuilder.DELETE, newcode, DepRoboRIO.getJar(LIBS_THIN)); } /** * Builds the current project against the roboRIO support classes, and * combines it with the roboRIO support libraries. * * This expects that the current project has a <code>src</code> directory. * * @param main the main class of the application. * @return the resulting Artifact of everything combined. * @throws IOException if the build or combination fails. */ public static Artifact buildProject(Class<? extends FRCApplication> main) throws IOException { return build(DepProject.directory("src"), main); } }