/* * ShootOFF - Software for Laser Dry Fire Training * Copyright (C) 2016 phrack * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.shootoff; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.URL; import java.net.UnknownHostException; import java.nio.file.Files; import java.util.Enumeration; import java.util.Optional; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.shootoff.camera.CameraFactory; import com.shootoff.camera.cameratypes.OptiTrackCamera; import com.shootoff.camera.cameratypes.PS3EyeCamera; import com.shootoff.config.Configuration; import com.shootoff.config.ConfigurationException; import com.shootoff.gui.controller.ShootOFFController; import com.shootoff.headless.HeadlessController; import com.shootoff.plugins.TextToSpeech; import com.shootoff.util.HardwareData; import com.shootoff.util.SystemInfo; import com.shootoff.util.VersionChecker; import com.sun.deploy.uitoolkit.impl.fx.HostServicesFactory; import com.sun.javafx.application.HostServicesDelegate; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javafx.application.Application; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.fxml.FXMLLoader; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.CheckBox; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.ProgressIndicator; import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.paint.Color; import javafx.stage.Modality; import javafx.stage.Stage; public class Main extends Application { private static final Logger logger = LoggerFactory.getLogger(Main.class); private static final int MINIMUM_CPU_SCORE_EXCELLENT = 4000; private static final int MINIMUM_CPU_SCORE_PASSABLE = 3000; private static final long MINIMUM_RAM_EXCELLENT = 11712; // MB private static final long MINIMUM_RAM_PASSABLE = 4096; // MB private static final String POOR_HARDWARE_MESSAGE = "Hardware Status: Poor -- Based on the data we gathered,\nShootOFF will likely not run well on this machine."; private static final String PASSABLE_HARDWARE_MESSAGE = "Hardware Status: Passable -- Based on the data we gathered,\nShootOFF will likely run OK on this machine but may miss\noccasional shots."; private static final String EXCELLENT_HARDWARE_MESSAGE = "Hardware Status: Excellent -- Based on the data we gathered,\nthis machine should have no problems running ShootOFF."; public static final String SHOOTOFF_DOMAIN = "http://shootoffapp.com/"; private boolean isJWS = false; private static final String RESOURCES_METADATA_NAME = "shootoff-writable-resources.xml"; private static final String RESOURCES_JAR_NAME = "shootoff-writable-resources.jar"; private File resourcesMetadataFile; private File resourcesJARFile; private Stage primaryStage; private static final String VERSION_METADATA_NAME = "shootoff-version.xml"; private static Optional<String> version = Optional.empty(); private static boolean shouldShowV4lWarning = false; protected static class ResourcesInfo { private final String version; private final long fileSize; private final String xml; public ResourcesInfo(final String version, final long fileSize, final String xml) { this.version = version; this.fileSize = fileSize; this.xml = xml; } public String getVersion() { return version; } public long getFileSize() { return fileSize; } public String getXML() { return xml; } } private Optional<String> parseField(String metadataXML, String tagName, String fieldName) { final String tag = "<" + tagName; int tagStart = metadataXML.indexOf(tag); if (tagStart == -1) { if (logger.isErrorEnabled()) logger.error("Couldn't parse " + tag + " tag from metadata"); if (isJWS) tryRunningShootOFF(); return Optional.empty(); } tagStart += tag.length(); fieldName += "=\""; int dataStart = metadataXML.indexOf(fieldName, tagStart); if (dataStart == -1) { logger.error("Couldn't parse {} field from metadata", fieldName); if (isJWS) tryRunningShootOFF(); return Optional.empty(); } dataStart += fieldName.length(); final int dataEnd = metadataXML.indexOf("\"", dataStart); return Optional.of(metadataXML.substring(dataStart, dataEnd)); } protected Optional<ResourcesInfo> deserializeMetadataXML(final String metadataXML) { final Optional<String> version = parseField(metadataXML, "resources", "version"); final Optional<String> fileSize = parseField(metadataXML, "resources", "fileSize"); if (version.isPresent() && fileSize.isPresent()) { return Optional.of(new ResourcesInfo(version.get(), Long.parseLong(fileSize.get()), metadataXML)); } return Optional.empty(); } private Optional<ResourcesInfo> getWebstartResourcesInfo(final File metadataFile) { if (!metadataFile.exists()) { logger.error("Local metadata file unavailable"); return Optional.empty(); } try { final String metadataXML = new String(Files.readAllBytes(metadataFile.toPath()), "UTF-8"); return deserializeMetadataXML(metadataXML); } catch (final IOException e) { logger.error("Error reading metadata XML for JNLP", e); } return Optional.empty(); } private Optional<ResourcesInfo> getWebstartResourcesInfo(final String metadataAddress) { HttpURLConnection connection = null; InputStream stream = null; try { connection = (HttpURLConnection) new URL(metadataAddress).openConnection(); stream = connection.getInputStream(); } catch (final UnknownHostException e) { if (logger.isErrorEnabled()) logger.error("Could not connect to remote host " + e.getMessage() + " to download writable resources.", e); tryRunningShootOFF(); return Optional.empty(); } catch (final IOException e) { if (connection != null) connection.disconnect(); logger.error("Error downloading writable resources file", e); tryRunningShootOFF(); return Optional.empty(); } final StringBuilder metadataXML = new StringBuilder(); try (BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) { String line; while ((line = br.readLine()) != null) { if (metadataXML.length() > 0) metadataXML.append("\n"); metadataXML.append(line); } } catch (final IOException e) { connection.disconnect(); logger.error("Failed to read resources metadata", e); tryRunningShootOFF(); return Optional.empty(); } connection.disconnect(); return deserializeMetadataXML(metadataXML.toString()); } /** * Writable resources (e.g. shootoff.properties, sounds, targets, etc.) * cannot be included in JAR files for a Webstart applications, thus we * download them from a remote URL and extract them locally if necessary. * * Downloads the file at fileAddress with the assumption that it is a JAR * containing writable resources. If there is an existing JAR with writable * resources we only do the download if the file sizes are different. * * @param fileAddress * the url (e.g. http://example.com/file.jar) that contains * ShootOFF's writable resources */ private void downloadWebstartResources(ResourcesInfo ri, String fileAddress) { HttpURLConnection connection = null; InputStream stream = null; try { connection = (HttpURLConnection) new URL(fileAddress).openConnection(); stream = connection.getInputStream(); } catch (final UnknownHostException e) { logger.error("Could not connect to remote host " + e.getMessage() + " to download writable resources.", e); tryRunningShootOFF(); return; } catch (final IOException e) { if (connection != null) connection.disconnect(); logger.error("Failed to get stream to download writable resources file", e); tryRunningShootOFF(); return; } final long remoteFileLength = ri.getFileSize(); if (remoteFileLength == 0) { logger.error("Remote writable resources file query returned 0 len."); connection.disconnect(); tryRunningShootOFF(); return; } final InputStream remoteStream = stream; final Task<Boolean> task = new Task<Boolean>() { @Override public Boolean call() throws InterruptedException { final BufferedInputStream bufferedInputStream = new BufferedInputStream(remoteStream); try (FileOutputStream fileOutputStream = new FileOutputStream(resourcesJARFile)) { long totalDownloaded = 0; int count; final byte buffer[] = new byte[1024]; while ((count = bufferedInputStream.read(buffer, 0, buffer.length)) != -1) { fileOutputStream.write(buffer, 0, count); totalDownloaded += count; updateProgress(((double) totalDownloaded / (double) remoteFileLength) * 100, 100); } updateProgress(100, 100); } catch (final IOException e) { logger.error("Failed to download writable resources file", e); return false; } return true; } }; final ProgressDialog progressDialog = new ProgressDialog("Downloading Resources...", "Downloading required resources (targets, sounds, etc.)...", task); final HttpURLConnection con = connection; task.setOnSucceeded((value) -> { progressDialog.close(); con.disconnect(); if (task.getValue()) { try { final PrintWriter out = new PrintWriter(resourcesMetadataFile, "UTF-8"); out.print(ri.getXML()); out.close(); } catch (final IOException e) { if (logger.isErrorEnabled()) logger.error("Could't update metadata file: " + e.getMessage(), e); } extractWebstartResources(); } else { tryRunningShootOFF(); } }); new Thread(task, "DownloadJNLPResources").start(); } /** * If we could not acquire writable resources for Webstart, see if we have * enough to run anyway. */ private void tryRunningShootOFF() { if (!new File(System.getProperty("shootoff.home") + File.separator + "shootoff.properties").exists()) { final Alert resourcesAlert = new Alert(AlertType.ERROR); resourcesAlert.setTitle("Missing Resources"); resourcesAlert.setHeaderText("Missing Required Resources!"); resourcesAlert.setResizable(true); resourcesAlert.setContentText("ShootOFF could not acquire the necessary resources to run. Please ensure " + "you have a connection to the Internet and can connect to http://shootoffapp.com and try again.\n\n" + "If you cannot get the browser-launched version of ShootOFF to work, use the standlone version from " + "the website."); resourcesAlert.showAndWait(); } else { runShootOFF(); } } private void extractWebstartResources() { final Task<Boolean> task = new Task<Boolean>() { @Override protected Boolean call() throws Exception { JarFile jar = null; try { jar = new JarFile(resourcesJARFile); Enumeration<JarEntry> enumEntries = jar.entries(); int fileCount = 0; while (enumEntries.hasMoreElements()) { final JarEntry entry = enumEntries.nextElement(); if (!entry.getName().startsWith("META-INF") && !entry.isDirectory()) fileCount++; } enumEntries = jar.entries(); int currentCount = 0; while (enumEntries.hasMoreElements()) { final JarEntry entry = enumEntries.nextElement(); if (entry.getName().startsWith("META-INF")) continue; final File f = new File(System.getProperty("shootoff.home") + File.separator + entry.getName()); if (entry.isDirectory()) { if (!f.exists() && !f.mkdir()) { final IOException e = new IOException( "Failed to make directory while extracting JAR: " + entry.getName()); logger.error("Error making directory to extract writable JAR contents", e); throw e; } } else { final InputStream is = jar.getInputStream(entry); try (FileOutputStream fos = new FileOutputStream(f)) { while (is.available() > 0) { fos.write(is.read()); } } is.close(); currentCount++; updateProgress(((double) currentCount / (double) fileCount) * 100, 100); } } updateProgress(100, 100); } catch (final IOException e) { logger.error("Error extracting writable resources file for JNLP", e); return false; } finally { try { if (jar != null) jar.close(); } catch (final IOException e) { logger.error("Error closing writable resources file for JNLP", e); } } return true; } }; final ProgressDialog progressDialog = new ProgressDialog("Extracting Resources...", "Extracting required resources (targets, sounds, etc.)...", task); task.setOnSucceeded((value) -> { progressDialog.close(); if (task.getValue()) { runShootOFF(); } else { tryRunningShootOFF(); } }); new Thread(task, "ExtractJNLPResources").start(); } public static class ProgressDialog { private final Stage stage = new Stage(); private final Label messageLabel = new Label(); private final ProgressBar pb = new ProgressBar(); private final ProgressIndicator pin = new ProgressIndicator(); public ProgressDialog(String dialogTitle, String dialogMessage, final Task<?> task) { stage.setTitle(dialogTitle); stage.initModality(Modality.APPLICATION_MODAL); pb.setProgress(-1F); pin.setProgress(-1F); messageLabel.setText(dialogMessage); final HBox hb = new HBox(); hb.setSpacing(5); hb.setAlignment(Pos.CENTER); hb.getChildren().addAll(pb, pin); pb.prefWidthProperty().bind(hb.widthProperty().subtract(hb.getSpacing() * 6)); final BorderPane bp = new BorderPane(); bp.setTop(messageLabel); bp.setBottom(hb); final Scene scene = new Scene(bp); stage.setScene(scene); stage.show(); pb.progressProperty().bind(task.progressProperty()); pin.progressProperty().bind(task.progressProperty()); } public void close() { stage.close(); } } @SuppressFBWarnings("DM_EXIT") public static void forceClose(final int status) { System.exit(status); } private Optional<String> getVersionXML(final String versionAddress) { HttpURLConnection connection = null; InputStream stream = null; try { connection = (HttpURLConnection) new URL(versionAddress).openConnection(); stream = connection.getInputStream(); } catch (final UnknownHostException e) { logger.error("Could not connect to remote host " + e.getMessage() + " to download version metadata.", e); return Optional.empty(); } catch (final IOException e) { if (connection != null) connection.disconnect(); logger.error("Error downloading version metadata", e); return Optional.empty(); } final StringBuilder versionXML = new StringBuilder(); try (BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) { String line; while ((line = br.readLine()) != null) { if (versionXML.length() > 0) versionXML.append("\n"); versionXML.append(line); } } catch (final IOException e) { connection.disconnect(); logger.error("Failed to read version metadata", e); return Optional.empty(); } connection.disconnect(); return Optional.of(versionXML.toString()); } public void checkVersion() { final Optional<String> versionXML = getVersionXML(SHOOTOFF_DOMAIN + VERSION_METADATA_NAME); if (versionXML.isPresent()) { final Optional<String> stableVersion = parseField(versionXML.get(), "stableRelease", "version"); if (stableVersion.isPresent() && VersionChecker.compareVersions(stableVersion.get(), version.get()) > 0) { final Optional<String> downloadLink = parseField(versionXML.get(), "stableRelease", "download"); final String link; if (downloadLink.isPresent()) link = downloadLink.get(); else link = SHOOTOFF_DOMAIN; final Alert shootoffWelcome = new Alert(AlertType.INFORMATION); shootoffWelcome.setTitle("ShootOFF Updated"); shootoffWelcome.setHeaderText("This version of ShootOFF is outdated!"); shootoffWelcome.setResizable(true); final FlowPane fp = new FlowPane(); final Label lbl = new Label( "The current stable release of ShootOFF is " + stableVersion.get() + ", but you are running " + version.get() + ". " + "You can download the current version of ShootOFF here:\n\n"); final Hyperlink lnk = new Hyperlink(link); lnk.setOnAction((event) -> { final HostServicesDelegate hostServices = HostServicesFactory.getInstance(this); hostServices.showDocument(link); lnk.setVisited(true); }); fp.getChildren().addAll(lbl, lnk); shootoffWelcome.getDialogPane().contentProperty().set(fp); shootoffWelcome.showAndWait(); } else if (stableVersion.isPresent() && stableVersion.get().compareTo(version.get()) < 0) { logger.warn("Future version of ShootOFF? stableVersion = {}, this.version = {}", stableVersion.get(), version.get()); } else { logger.debug("ShootOFF is up to date"); } } } public void runShootOFF() { final String[] args = getParameters().getRaw().toArray(new String[getParameters().getRaw().size()]); Configuration config; try { config = new Configuration(System.getProperty("shootoff.home") + File.separator + "shootoff.properties", args); } catch (IOException | ConfigurationException e) { logger.error("Error fetching ShootOFF configuration to run ShootOFF", e); return; } if (version.isPresent() && !config.inDebugMode() && !isJWS) checkVersion(); // This initializes the TTS engine TextToSpeech.say(""); if (config.isFirstRun()) { if (shouldShowV4lWarning) showV4lWarning(); if (config.isHeadless()) { config.setUseErrorReporting(false); } else { config.setUseErrorReporting(showFirstRunMessage()); } config.setFirstRun(false); try { config.writeConfigurationFile(); } catch (ConfigurationException | IOException e) { logger.error("Error persisting firstrun = false in config", e); } } // This simply ensures that error reporting is turned off, // once it's off it stays off if (!config.useErrorReporting() || config.inDebugMode()) { Configuration.disableErrorReporting(); logger.info("Error reporting has been disabled."); } if (config.isHeadless()) { new HeadlessController(); } else { startGui(config); } } private void startGui(Configuration config) { try { final FXMLLoader loader = new FXMLLoader(Main.class.getResource("/com/shootoff/gui/ShootOFF.fxml")); loader.load(); final Scene scene = new Scene(loader.getRoot()); if (version.isPresent()) primaryStage.setTitle("ShootOFF " + version.get()); else primaryStage.setTitle("ShootOFF"); primaryStage.setScene(scene); final ShootOFFController controller = (ShootOFFController) loader.getController(); controller.init(config); primaryStage.show(); } catch (final IOException e) { logger.error("Error loading ShootOFF FXML file", e); return; } } private void setHardwareMessage(Label hardwareMessageLabel, int cpuScore) { if (cpuScore < MINIMUM_CPU_SCORE_PASSABLE) { hardwareMessageLabel.setText(POOR_HARDWARE_MESSAGE); hardwareMessageLabel.setTextFill(Color.RED); } else if (cpuScore < MINIMUM_CPU_SCORE_EXCELLENT) { hardwareMessageLabel.setText(PASSABLE_HARDWARE_MESSAGE); hardwareMessageLabel.setTextFill(Color.GOLD); } else { hardwareMessageLabel.setText(EXCELLENT_HARDWARE_MESSAGE); hardwareMessageLabel.setTextFill(Color.DARKGREEN); } } private void setHardwareMessage(Label hardwareMessageLabel, long installedRam) { if (installedRam < MINIMUM_RAM_PASSABLE) { hardwareMessageLabel.setText(POOR_HARDWARE_MESSAGE); hardwareMessageLabel.setTextFill(Color.RED); } else if (installedRam < MINIMUM_RAM_EXCELLENT) { hardwareMessageLabel.setText(PASSABLE_HARDWARE_MESSAGE); hardwareMessageLabel.setTextFill(Color.GOLD); } else { hardwareMessageLabel.setText(EXCELLENT_HARDWARE_MESSAGE); hardwareMessageLabel.setTextFill(Color.DARKGREEN); } } private void showV4lWarning() { final Alert v4lWarning = new Alert(AlertType.WARNING); v4lWarning.setTitle("v4lcompat May Be Required"); v4lWarning.setHeaderText("If you have camera problems, you may need to preload v4lcompat"); v4lWarning.setContentText("You are running Linux but ShootOFF was not successful in determining " + "if your webcam(s) require v4lcompat. If you have any trouble using your cameras, preload " + "v4lcompat.so with the following command:\n\n" + "export LD_PRELOAD=path_to_v4lcompat; java -jar ShootOFF.jar\n\n" + "You can use the following command to find where vl41compat.so is on your system:\n\n" + "find /usr/lib -name \"v4l1compat.so\""); v4lWarning.showAndWait(); } private boolean showFirstRunMessage() { final Label hardwareMessageLabel = new Label("Fetching hardware status to determine how well ShootOFF\n" + "will run on this machine. This may take a moment..."); new Thread(() -> { final String cpuName = HardwareData.getCpuName(); final Optional<Integer> cpuScore = HardwareData.getCpuScore(); final long installedRam = HardwareData.getMegabytesOfRam(); if (cpuScore.isPresent()) { if (logger.isDebugEnabled()) logger.debug("Processor: {}, Processor Score: {}, installed RAM: {} MB", cpuName, cpuScore.get(), installedRam); Platform.runLater(() -> setHardwareMessage(hardwareMessageLabel, cpuScore.get())); } else { if (logger.isDebugEnabled()) logger.debug("Processor: {}, installed RAM: {} MB", cpuName, installedRam); Platform.runLater(() -> setHardwareMessage(hardwareMessageLabel, installedRam)); } }).start(); final Alert shootoffWelcome = new Alert(AlertType.INFORMATION); shootoffWelcome.setTitle("Welcome to ShootOFF"); shootoffWelcome.setHeaderText("Please Ensure Your Firearm is Unloaded!"); shootoffWelcome.setResizable(true); final FlowPane fp = new FlowPane(); final Label lbl = new Label("Thank you for choosing ShootOFF for your training needs.\n" + "Please be careful to ensure your firearm is not loaded\n" + "every time you use ShootOFF. We are not liable for any\n" + "negligent discharges that may result from your use of this\n" + "software.\n\n" + "We upload most errors that cause crashes to our servers to\n" + "help us detect and fix common problems. We do not include any\n" + "personal information in these reports, but you may uncheck\n" + "the box below if you do not want to support this effort.\n\n"); final CheckBox useErrorReporting = new CheckBox("Allow ShootOFF to Send Error Reports"); useErrorReporting.setSelected(true); fp.getChildren().addAll(hardwareMessageLabel, lbl, useErrorReporting); shootoffWelcome.getDialogPane().contentProperty().set(fp); shootoffWelcome.showAndWait(); return useErrorReporting.isSelected(); } public static void closeNoCamera() { final Alert cameraAlert = new Alert(AlertType.ERROR); cameraAlert.setTitle("No Webcams"); cameraAlert.setHeaderText("No Webcams Found!"); cameraAlert.setResizable(true); cameraAlert.setContentText("ShootOFF needs a webcam to function. Now closing..."); cameraAlert.showAndWait(); Main.forceClose(-1); } public static void closeNoV4lCompat(File v4lCompat) { logger.error("This system uses Video4Linux, but v4lcompat is not preloaded. " + "Run the following command then run ShootOFF again: " + "export LD_PRELOAD=\"" + v4lCompat.getPath() + "\""); Main.forceClose(-1); } @Override public void start(Stage primaryStage) { OptiTrackCamera.init(); this.primaryStage = primaryStage; if (SystemInfo.isMacOsX() && CameraFactory.getWebcams().isEmpty()) { closeNoCamera(); } else if (SystemInfo.isWindows()) { PS3EyeCamera.init(); } if (System.getProperty("javawebstart.version", null) != null) { isJWS = true; final File shootoffHome = new File(System.getProperty("user.home") + File.separator + ".shootoff"); if (!shootoffHome.exists() && !shootoffHome.mkdirs()) { final Alert homeAlert = new Alert(AlertType.ERROR); homeAlert.setTitle("No ShootOFF Home"); homeAlert.setHeaderText("Missing ShootOFF's Home Directory!"); homeAlert.setResizable(true); homeAlert.setContentText("ShootOFF's home directory " + shootoffHome.getPath() + " " + "does not exist and could not be created. Now closing..."); homeAlert.showAndWait(); return; } if (System.getProperty("shootoff.home") == null) { System.setProperty("shootoff.home", shootoffHome.getAbsolutePath()); } System.setProperty("shootoff.sessions", System.getProperty("shootoff.home") + File.separator + "sessions"); System.setProperty("shootoff.courses", System.getProperty("shootoff.home") + File.separator + "courses"); System.setProperty("shootoff.plugins", System.getProperty("shootoff.home") + File.separator + "exercises"); resourcesMetadataFile = new File( System.getProperty("shootoff.home") + File.separator + RESOURCES_METADATA_NAME); final Optional<ResourcesInfo> localRI = getWebstartResourcesInfo(resourcesMetadataFile); final Optional<ResourcesInfo> remoteRI = getWebstartResourcesInfo( SHOOTOFF_DOMAIN + "jws/" + RESOURCES_METADATA_NAME); if (!localRI.isPresent() && remoteRI.isPresent()) { resourcesJARFile = new File(System.getProperty("shootoff.home") + File.separator + RESOURCES_JAR_NAME); downloadWebstartResources(remoteRI.get(), "http://shootoffapp.com/jws/" + RESOURCES_JAR_NAME); } else if (localRI.isPresent() && remoteRI.isPresent()) { if (localRI.get().getVersion().equals(remoteRI.get().getVersion())) { runShootOFF(); } else { System.out.println(String.format("Local version: %s, Remote version: %s", localRI.get().getVersion(), remoteRI.get().getVersion())); resourcesJARFile = new File( System.getProperty("shootoff.home") + File.separator + RESOURCES_JAR_NAME); downloadWebstartResources(remoteRI.get(), "http://shootoffapp.com/jws/" + RESOURCES_JAR_NAME); } } else { logger.error("Could not locate local or remote resources metadata"); } } else { if (System.getProperty("shootoff.home") == null) { System.setProperty("shootoff.home", System.getProperty("user.dir")); } System.setProperty("shootoff.sessions", System.getProperty("shootoff.home") + File.separator + "sessions"); System.setProperty("shootoff.courses", System.getProperty("shootoff.home") + File.separator + "courses"); System.setProperty("shootoff.plugins", System.getProperty("shootoff.home") + File.separator + "exercises"); runShootOFF(); } } public static Optional<String> getVersion() { return version; } @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME") public static void main(String[] args) { // Check the comment at the top of the Camera class // for more information about this hack if (SystemInfo.isMacOsX()) { nu.pattern.OpenCV.loadShared(); CameraFactory.getDefault(); } else if (SystemInfo.isLinux()) { // Need to ensure v4l1compat is preloaded if it exists otherwise // OpenCV won't work final File v4lCompat = new File("/usr/lib/libv4l/v4l1compat.so"); if (v4lCompat.exists()) { final String preload = System.getenv("LD_PRELOAD"); if (preload == null || !preload.contains(v4lCompat.getPath())) { closeNoV4lCompat(v4lCompat); } } else { // The over-exuberance here is because a lot of people miss this // message logger.warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + "This system is running Linux, and likely therefore also v4l. " + "If ShootOFF fails to run or has camera problems, it's likely because you need " + "to preload v4l1compat using: " + "export LD_PRELOAD=path_to_v4l1compat; java -jar ShootOFF.jar\n" + "!!!!!!!!!!!!!!!!!!!!!!!!!!!"); shouldShowV4lWarning = true; } } nu.pattern.OpenCV.loadShared(); // Read ShootOFF's version number final Properties prop = new Properties(); try (InputStream inputStream = Main.class.getResourceAsStream("/version.properties")) { prop.load(inputStream); version = Optional.of(prop.getProperty("version")); } catch (final IOException ioe) { logger.error("Couldn't read version properties", ioe); } launch(args); } }