22
33import com .google .common .eventbus .EventBus ;
44import com .google .common .eventbus .Subscribe ;
5+ import com .google .common .hash .Hashing ;
56import com .google .common .io .LineReader ;
7+ import com .google .common .io .Resources ;
68import edu .wpi .grip .core .Pipeline ;
79import edu .wpi .grip .core .events .ProjectSettingsChangedEvent ;
810import edu .wpi .grip .core .events .StopPipelineEvent ;
911import edu .wpi .grip .core .serialization .Project ;
1012import edu .wpi .grip .core .settings .ProjectSettings ;
1113import edu .wpi .grip .ui .util .StringInMemoryFile ;
1214import javafx .application .Platform ;
15+ import javafx .beans .binding .Bindings ;
1316import javafx .beans .property .BooleanProperty ;
14- import javafx .beans .property .SimpleBooleanProperty ;
17+ import javafx .beans .property .StringProperty ;
1518import javafx .fxml .FXML ;
16- import javafx .scene .control .*;
19+ import javafx .scene .control .Label ;
20+ import javafx .scene .control .ProgressIndicator ;
21+ import javafx .scene .control .TextArea ;
22+ import javafx .scene .control .TextField ;
1723import net .schmizz .sshj .SSHClient ;
1824import net .schmizz .sshj .common .StreamCopier ;
1925import net .schmizz .sshj .connection .channel .direct .Session ;
2329import net .schmizz .sshj .xfer .scp .SCPFileTransfer ;
2430
2531import javax .inject .Inject ;
26- import java .io .IOException ;
27- import java .io .InputStreamReader ;
28- import java .io .InterruptedIOException ;
29- import java .io .StringWriter ;
32+ import java .io .*;
33+ import java .net .URL ;
3034import java .net .URLDecoder ;
3135import java .net .UnknownHostException ;
3236import java .util .Optional ;
4246 */
4347public class DeployController {
4448 private final static String GRIP_JAR = "grip.jar" ;
45- private final static String PROJECT_FILE = "project. grip" ;
46- private final static String LOCAL_GRIP_JAR = URLDecoder . decode (
47- Project . class . getProtectionDomain (). getCodeSource (). getLocation () .getPath ());
49+ private final static String GRIP_WRAPPER = "grip" ;
50+ private final static URL LOCAL_GRIP_URL = Project . class . getProtectionDomain (). getCodeSource (). getLocation ();
51+ private final static String LOCAL_GRIP_PATH = URLDecoder . decode ( LOCAL_GRIP_URL .getPath ());
4852
4953 @ FXML private TextField address ;
5054 @ FXML private TextField user ;
5155 @ FXML private TextField password ;
5256 @ FXML private TextField javaHome ;
57+ @ FXML private TextField jvmArgs ;
5358 @ FXML private TextField deployDir ;
59+ @ FXML private TextField projectFile ;
5460 @ FXML private ProgressIndicator progress ;
55- @ FXML private Button deployButton ;
5661 @ FXML private Label status ;
5762 @ FXML private TextArea console ;
63+ @ FXML private BooleanProperty deploying ;
64+ @ FXML private StringProperty command ;
5865
5966 @ Inject private EventBus eventBus ;
6067 @ Inject private Project project ;
6168 @ Inject private Pipeline pipeline ;
6269 @ Inject private Logger logger ;
6370
64- private final BooleanProperty deploying = new SimpleBooleanProperty (this , "deploying" , false );
6571 private Optional <Thread > deployThread = Optional .empty ();
6672
6773 @ FXML
6874 public void initialize () {
69- loadSettings (pipeline .getProjectSettings ());
70-
71- deployButton .disableProperty ().bind (deploying );
72- progress .disableProperty ().bind (deploying .not ());
7375 deploying .addListener ((o , b , d ) -> progress .setProgress (d ? ProgressIndicator .INDETERMINATE_PROGRESS : 0 ));
76+ command .bind (Bindings .concat (javaHome .textProperty (), "/bin/java " , jvmArgs .textProperty (), " -jar '" ,
77+ deployDir .textProperty (), "/" , GRIP_JAR , "' '" , deployDir .textProperty (), "/" , projectFile .textProperty (), "'" ));
78+
79+ loadSettings (pipeline .getProjectSettings ());
7480 }
7581
7682 @ Subscribe
@@ -85,6 +91,7 @@ private void loadSettings(ProjectSettings settings) {
8591 address .setText (settings .getDeployAddress ());
8692 user .setText (settings .getDeployUser ());
8793 javaHome .setText (settings .getDeployJavaHome ());
94+ jvmArgs .setText (settings .getDeployJvmOptions ());
8895 deployDir .setText (settings .getDeployDir ());
8996 }
9097
@@ -95,6 +102,7 @@ private void saveSettings() {
95102 settings .setDeployAddress (address .getText ());
96103 settings .setDeployUser (user .getText ());
97104 settings .setDeployJavaHome (javaHome .getText ());
105+ settings .setDeployJvmOptions (jvmArgs .getText ());
98106 settings .setDeployDir (deployDir .getText ());
99107 eventBus .post (new ProjectSettingsChangedEvent (settings ));
100108 }
@@ -107,8 +115,7 @@ public void onDeploy() {
107115 console .clear ();
108116
109117 // Start the deploy in a new thread, so the GUI doesn't freeze
110- deployThread = Optional .of (new Thread (() ->
111- deploy (address .getText (), user .getText (), password .getText (), javaHome .getText (), deployDir .getText ())));
118+ deployThread = Optional .of (new Thread (this ::deploy , "Deploy" ));
112119 deployThread .get ().setDaemon (true );
113120 deployThread .get ().start ();
114121 }
@@ -124,13 +131,13 @@ public void onStop() {
124131 * Upload and run the GRIP project using the current deploy settings. This is run in a separate thread, and it
125132 * periodically updates the GUI to inform the user of the current status of the deployment.
126133 */
127- private void deploy (String address , String user , String password , String javaHome , String deployDir ) {
128- setStatusAsync ("Connecting to " + address , false );
134+ private void deploy () {
135+ setStatusAsync ("Connecting to " + address . getText () , false );
129136
130137 try (SSHClient ssh = new SSHClient ()) {
131138 ssh .addHostKeyVerifier ((hostname , port , key ) -> true );
132- ssh .connect (address );
133- ssh .authPassword (user , password );
139+ ssh .connect (address . getText () );
140+ ssh .authPassword (user . getText () , password . getText () );
134141
135142 // Update the progress bar and status text while uploading files
136143 SCPFileTransfer scp = ssh .newSCPFileTransfer ();
@@ -150,20 +157,43 @@ public StreamCopier.Listener file(String name, long size) {
150157 StringWriter projectWriter = new StringWriter ();
151158 project .save (projectWriter );
152159
153- // Upload the GRIP core JAR and the serialized project to the robot
154- scp .upload (new StringInMemoryFile (PROJECT_FILE , projectWriter .toString ()), deployDir + "/" );
155- scp .upload (new FileSystemFile (LOCAL_GRIP_JAR ), deployDir + "/" + GRIP_JAR );
160+ final String commandStr = command .get ();
161+ final String pathStr = deployDir .getText () + "/" ;
162+
163+
164+ // Upload the core GRIP JAR only if there isn't already one with the same hash on the robot. This prevents
165+ // us from redundantly deploying the same JAR over and over again (so deploy times after the first are
166+ // much faster), while still ensuring that the JAR is deployed if it has to be.
167+ try (Session session = ssh .startSession ()) {
168+ Session .Command md5Cmd = session .exec ("md5sum " + pathStr + GRIP_JAR );
169+ String remoteMd5Sum = new DataInputStream (md5Cmd .getInputStream ()).readLine ();
170+ String localMd5Sum = Resources .asByteSource (LOCAL_GRIP_URL ).hash (Hashing .md5 ()).toString ();
171+
172+ // If the MD5 sum doesn't match up or there's any kind of error (there isn't a JAR there, etc...),
173+ // just upload the JAR.
174+ if (remoteMd5Sum == null || remoteMd5Sum .length () < 32 || !remoteMd5Sum .substring (0 , 32 ).equals (localMd5Sum )) {
175+ scp .upload (new FileSystemFile (LOCAL_GRIP_PATH ), pathStr + GRIP_JAR );
176+ } else {
177+ logger .info ("md5sum " + GRIP_JAR + " matches. Skipping upload." );
178+ }
179+ }
180+
181+ // Upload the project file and a wrapper script with the JVM arguments. These are very small and change
182+ // often, so we might as well upload them every time.
183+ scp .upload (new StringInMemoryFile (GRIP_WRAPPER , "echo \" " + commandStr + "\" \n " + commandStr , 0755 ), pathStr );
184+ scp .upload (new StringInMemoryFile (projectFile .getText (), projectWriter .toString ()), pathStr );
156185
157186 // Stop the pipeline before running it remotely, so the two instances of GRIP don't try to publish to the
158187 // same NetworkTables keys.
159188 eventBus .post (new StopPipelineEvent ());
160189
161190 // Run the project!
162191 setStatusAsync ("Running GRIP" , false );
192+ logger .info ("Running " + commandStr );
193+
163194 Session session = ssh .startSession ();
164195 session .allocateDefaultPTY ();
165- Session .Command cmd = session .exec (javaHome + "/bin/java -jar " + deployDir + "/" + GRIP_JAR + "' '"
166- + deployDir + "/" + PROJECT_FILE + "'" );
196+ Session .Command cmd = session .exec (String .format ("'%s/%s'" , pathStr , GRIP_WRAPPER ));
167197
168198 LineReader inputReader = new LineReader (new InputStreamReader (cmd .getInputStream ()));
169199 while (isNotCanceled ()) {
@@ -175,7 +205,7 @@ public StreamCopier.Listener file(String name, long size) {
175205 Platform .runLater (() -> console .setText (console .getText () + line + "\n " ));
176206 }
177207 } catch (UnknownHostException e ) {
178- setStatusAsync ("Unknown host: " + address , true );
208+ setStatusAsync ("Unknown host: " + address . getText () , true );
179209 } catch (UserAuthException e ) {
180210 setStatusAsync ("Invalid username or password (should be \" lvuser\" and blank for roboRIO)" , true );
181211 } catch (InterruptedIOException e ) {
0 commit comments