diff --git a/app/src/cc/arduino/packages/UploaderFactory.java b/app/src/cc/arduino/packages/UploaderFactory.java index 36a4fbbf3..ebf364874 100644 --- a/app/src/cc/arduino/packages/UploaderFactory.java +++ b/app/src/cc/arduino/packages/UploaderFactory.java @@ -1,19 +1,15 @@ package cc.arduino.packages; -import processing.app.AbstractMonitor; -import processing.app.Base; -import processing.app.Constants; -import processing.app.NetworkMonitor; -import processing.app.SerialMonitor; -import processing.app.debug.TargetBoard; -import cc.arduino.packages.uploaders.HttpUploader; +import cc.arduino.packages.uploaders.SSHUploader; import cc.arduino.packages.uploaders.SerialUploader; +import processing.app.*; +import processing.app.debug.TargetBoard; public class UploaderFactory { public Uploader newUploader(TargetBoard board, String port) { - if ("true".equals(board.getPreferences().get("upload.via_http")) && Constants.IPV4_ADDRESS.matcher(port).find()) { - return new HttpUploader(port); + if ("true".equals(board.getPreferences().get("upload.via_ssh")) && Constants.IPV4_ADDRESS.matcher(port).find()) { + return new SSHUploader(port); } return new SerialUploader(); diff --git a/app/src/cc/arduino/packages/uploaders/HttpUploader.java b/app/src/cc/arduino/packages/uploaders/HttpUploader.java deleted file mode 100644 index 5b9d3cb59..000000000 --- a/app/src/cc/arduino/packages/uploaders/HttpUploader.java +++ /dev/null @@ -1,227 +0,0 @@ -package cc.arduino.packages.uploaders; - -import cc.arduino.packages.Uploader; -import com.jcraft.jsch.*; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.NameValuePair; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.protocol.Protocol; -import processing.app.Base; -import processing.app.Constants; -import processing.app.NetworkMonitor; -import processing.app.Preferences; -import processing.app.debug.EasySSLProtocolSocketFactory; -import processing.app.debug.RunnerException; -import processing.app.debug.TargetPlatform; -import processing.app.helpers.PreferencesMap; - -import java.io.*; -import java.util.regex.Matcher; - -import static processing.app.I18n._; - -public class HttpUploader extends Uploader { - - private static final String PROTOCOL = "https://"; - - static { - Protocol.registerProtocol("https", new Protocol("https", new EasySSLProtocolSocketFactory(), 443)); - } - - private final HttpClient client; - private final String ipAddress; - private final String baseUrl; - - public HttpUploader(String port) { - client = new HttpClient(); - Matcher matcher = Constants.IPV4_ADDRESS.matcher(port); - if (!matcher.find()) { - throw new IllegalArgumentException(port); - } - ipAddress = matcher.group(); - baseUrl = PROTOCOL + ipAddress + "/cgi-bin/luci/arduino"; - } - - public boolean requiresAuthorization() { - return true; - } - - public String getAuthorizationKey() { - return "runtime.pwd." + ipAddress; - } - - @Override - public boolean uploadUsingPreferences(String buildPath, String className, boolean usingProgrammer) throws RunnerException { - if (usingProgrammer) { - System.err.println(_("Http upload using programmer not supported")); - return false; - } - - client.getHttpConnectionManager().getParams().setConnectionTimeout(5000); - String password = Preferences.get(getAuthorizationKey()); - String auth = Base64.encodeBase64String(("root:" + password).getBytes()); - - int sleptTimes = 1; - while (boardNotReady(auth)) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - throw new RunnerException(e); - } - if (sleptTimes >= 3) { - throw new RunnerException(_("The board is not yet ready")); - } - sleptTimes += 1; - } - - try { - scpHexToBoard(buildPath, className); - } catch (Exception e) { - throw new RunnerException(e); - } - - TargetPlatform targetPlatform = Base.getTargetPlatform(); - PreferencesMap prefs = Preferences.getMap(); - prefs.putAll(Base.getBoardPreferences()); - prefs.putAll(targetPlatform.getTool(prefs.get("upload.tool"))); - - PostMethod post = new PostMethod(baseUrl + "/flash"); - post.setRequestHeader("Authorization", "Basic " + auth); - NameValuePair[] data = { - new NameValuePair("params", verbose ? prefs.get("upload.params.verbose") : prefs.get("upload.params.quiet")) - }; - post.setRequestBody(data); - - int statusCode; - try { - statusCode = client.executeMethod(post); - - System.err.println(post.getResponseBodyAsString()); - return statusCode == HttpStatus.SC_OK; - } catch (IOException e) { - throw new RunnerException(e); - } finally { - post.releaseConnection(); - } - } - - private void scpHexToBoard(String buildPath, String className) throws JSchException, IOException { - Session session = null; - Channel channel = null; - OutputStream out = null; - InputStream in = null; - try { - JSch jSch = new JSch(); - session = jSch.getSession("root", ipAddress, 22); - session.setPassword(Preferences.get(getAuthorizationKey())); - - session.setUserInfo(new NetworkMonitor.NoInteractionUserInfo()); - session.connect(30000); - - channel = session.openChannel("exec"); - ((ChannelExec) channel).setCommand("scp -t /tmp/sketch.hex"); - - out = channel.getOutputStream(); - in = channel.getInputStream(); - - channel.connect(); - - ensureAcknowledged(out, in); - - File hex = new File(buildPath, className + ".hex"); - - sendFileSizeAndName(out, in, hex); - ensureAcknowledged(out, in); - - sendFileContents(out, hex); - ensureAcknowledged(out, in); - } finally { - if (out != null) { - out.close(); - } - if (channel != null) { - channel.disconnect(); - } - if (session != null) { - session.disconnect(); - } - } - } - - private void ensureAcknowledged(OutputStream out, InputStream in) throws IOException { - out.flush(); - - int b = in.read(); - - if (b == 0) return; - if (b == -1) return; - - if (b == 1 || b == 2) { - StringBuilder sb = new StringBuilder(); - sb.append("SCP error: "); - - int c; - do { - c = in.read(); - sb.append((char) c); - } while (c != '\n'); - - throw new IOException(sb.toString()); - } - - throw new IOException("Uknown SCP error: " + b); - } - - private void sendFileContents(OutputStream out, File hex) throws IOException { - // send file contents - FileInputStream fis = null; - try { - fis = new FileInputStream(hex); - byte[] buf = new byte[4096]; - while (true) { - int len = fis.read(buf, 0, buf.length); - if (len <= 0) break; - out.write(buf, 0, len); //out.flush(); - } - - // send \0 (terminates file) - buf[0] = 0; - out.write(buf, 0, 1); - } finally { - if (fis != null) { - fis.close(); - } - } - } - - private void sendFileSizeAndName(OutputStream out, InputStream in, File hex) throws IOException { - // send "C0644 filesize filename" - long filesize = hex.length(); - String command = "C0644 " + filesize + " " + hex.getName() + "\n"; - out.write(command.getBytes()); - } - - protected boolean boardNotReady(String auth) throws RunnerException { - GetMethod get = new GetMethod(baseUrl + "/ready"); - get.setRequestHeader("Authorization", "Basic " + auth); - try { - int httpStatus = client.executeMethod(get); - if (httpStatus % HttpStatus.SC_BAD_REQUEST < 100 || httpStatus % HttpStatus.SC_INTERNAL_SERVER_ERROR < 100) { - System.err.println(get.getResponseBodyAsString()); - throw new RunnerException("Problem knowing if the board was ready"); - } - return httpStatus != HttpStatus.SC_OK; - } catch (IOException e) { - return true; - } - } - - @Override - public boolean burnBootloader() throws RunnerException { - throw new RunnerException("Can't burn bootloader via http"); - } - -} diff --git a/app/src/cc/arduino/packages/uploaders/SSHUploader.java b/app/src/cc/arduino/packages/uploaders/SSHUploader.java new file mode 100644 index 000000000..974a08645 --- /dev/null +++ b/app/src/cc/arduino/packages/uploaders/SSHUploader.java @@ -0,0 +1,266 @@ +package cc.arduino.packages.uploaders; + +import cc.arduino.packages.Uploader; +import com.jcraft.jsch.*; +import processing.app.Base; +import processing.app.Constants; +import processing.app.NetworkMonitor; +import processing.app.Preferences; +import processing.app.debug.RunnerException; +import processing.app.debug.TargetPlatform; +import processing.app.helpers.PreferencesMap; + +import java.io.*; +import java.util.regex.Matcher; + +import static processing.app.I18n._; + +public class SSHUploader extends Uploader { + + private final String ipAddress; + + public SSHUploader(String port) { + Matcher matcher = Constants.IPV4_ADDRESS.matcher(port); + if (!matcher.find()) { + throw new IllegalArgumentException(port); + } + ipAddress = matcher.group(); + } + + public boolean requiresAuthorization() { + return true; + } + + public String getAuthorizationKey() { + return "runtime.pwd." + ipAddress; + } + + @Override + public boolean uploadUsingPreferences(String buildPath, String className, boolean usingProgrammer) throws RunnerException { + if (usingProgrammer) { + System.err.println(_("Http upload using programmer not supported")); + return false; + } + + Session session = null; + try { + JSch jSch = new JSch(); + session = jSch.getSession("root", ipAddress, 22); + session.setPassword(Preferences.get(getAuthorizationKey())); + + session.setUserInfo(new NetworkMonitor.NoInteractionUserInfo()); + session.connect(30000); + + String uploadedSketchFile = new SCP(session).scpHexToBoard(buildPath, className); + + TargetPlatform targetPlatform = Base.getTargetPlatform(); + PreferencesMap prefs = Preferences.getMap(); + prefs.putAll(Base.getBoardPreferences()); + prefs.putAll(targetPlatform.getTool(prefs.get("upload.tool"))); + + return new SSHAVRDude(session).runAVRDude(uploadedSketchFile, verbose ? prefs.get("upload.params.verbose") : prefs.get("upload.params.quiet")); + } catch (JSchException e) { + if ("Auth cancel".equals(e.getMessage())) { + return false; + } + throw new RunnerException(e); + } catch (Exception e) { + throw new RunnerException(e); + } finally { + if (session != null) { + session.disconnect(); + } + } + } + + @Override + public boolean burnBootloader() throws RunnerException { + throw new RunnerException("Can't burn bootloader via SSH"); + } + + private static abstract class SSH { + + protected final Session session; + + public SSH(Session session) { + this.session = session; + } + + protected boolean execSyncCommand(String command) throws JSchException, IOException { + return execSyncCommand(command, false); + } + + protected boolean execSyncCommand(String command, boolean ignoreError) throws JSchException, IOException { + InputStream stdout = null; + InputStream stderr = null; + Channel channel = null; + try { + channel = session.openChannel("exec"); + ((ChannelExec) channel).setCommand(command); + + channel.setInputStream(null); + + stdout = channel.getInputStream(); + if (!ignoreError) { + stderr = ((ChannelExec) channel).getErrStream(); + } + + channel.connect(); + + int exitCode = consumeOutputSyncAndReturnExitCode(channel, stdout, stderr); + + return ignoreError || exitCode == 0; + + } finally { + if (stdout != null) { + stdout.close(); + } + if (stderr != null) { + stderr.close(); + } + if (channel != null) { + channel.disconnect(); + } + } + } + + protected int consumeOutputSyncAndReturnExitCode(Channel channel, InputStream stdout, InputStream stderr) throws IOException { + byte[] tmp = new byte[102400]; + while (true) { + while (stdout.available() > 0) { + int i = stdout.read(tmp, 0, tmp.length); + if (i < 0) break; + System.out.print(new String(tmp, 0, i)); + } + while (stderr != null && stderr.available() > 0) { + int i = stderr.read(tmp, 0, tmp.length); + if (i < 0) break; + System.err.print(new String(tmp, 0, i)); + } + if (channel.isClosed()) { + return channel.getExitStatus(); + } + try { + Thread.sleep(100); + } catch (Exception ee) { + // noop + } + } + } + + } + + private static class SSHAVRDude extends SSH { + + public SSHAVRDude(Session session) { + super(session); + } + + public boolean runAVRDude(String sketchFile, String additionalParams) throws IOException, JSchException { + boolean success = execSyncCommand("merge-sketch-with-bootloader " + sketchFile); + success = success && execSyncCommand("kill-bridge", true); + success = success && execSyncCommand("run-avrdude " + sketchFile + " '" + additionalParams + "'"); + return success; + } + + } + + private static class SCP extends SSH { + + private static final String SKETCH_FILE = "/tmp/sketch.hex"; + + public SCP(Session session) { + super(session); + } + + public String scpHexToBoard(String buildPath, String className) throws JSchException, IOException { + Channel channel = null; + OutputStream out = null; + InputStream in = null; + try { + channel = session.openChannel("exec"); + ((ChannelExec) channel).setCommand("scp -t " + SKETCH_FILE); + + out = channel.getOutputStream(); + in = channel.getInputStream(); + + channel.connect(); + + ensureAcknowledged(out, in); + + File hex = new File(buildPath, className + ".hex"); + + sendFileSizeAndName(out, in, hex); + ensureAcknowledged(out, in); + + sendFileContents(out, hex); + ensureAcknowledged(out, in); + + return SKETCH_FILE; + } finally { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + if (channel != null) { + channel.disconnect(); + } + } + } + + private void ensureAcknowledged(OutputStream out, InputStream in) throws IOException { + out.flush(); + + int b = in.read(); + + if (b == 0) return; + if (b == -1) return; + + if (b == 1 || b == 2) { + StringBuilder sb = new StringBuilder(); + sb.append("SCP error: "); + + int c; + do { + c = in.read(); + sb.append((char) c); + } while (c != '\n'); + + throw new IOException(sb.toString()); + } + + throw new IOException("Uknown SCP error: " + b); + } + + private void sendFileContents(OutputStream out, File hex) throws IOException { + FileInputStream fis = null; + try { + fis = new FileInputStream(hex); + byte[] buf = new byte[4096]; + while (true) { + int len = fis.read(buf, 0, buf.length); + if (len <= 0) break; + out.write(buf, 0, len); + } + + // \0 terminates file + buf[0] = 0; + out.write(buf, 0, 1); + } finally { + if (fis != null) { + fis.close(); + } + } + } + + private void sendFileSizeAndName(OutputStream out, InputStream in, File hex) throws IOException { + long filesize = hex.length(); + String command = "C0644 " + filesize + " " + hex.getName() + "\n"; + out.write(command.getBytes()); + } + + } + +} diff --git a/app/test/processing/app/debug/UploaderFactoryTest.java b/app/test/processing/app/debug/UploaderFactoryTest.java index 0f40b8066..a7a0c44af 100644 --- a/app/test/processing/app/debug/UploaderFactoryTest.java +++ b/app/test/processing/app/debug/UploaderFactoryTest.java @@ -1,12 +1,11 @@ package processing.app.debug; -import org.junit.Before; -import org.junit.Test; - import cc.arduino.packages.Uploader; import cc.arduino.packages.UploaderFactory; -import cc.arduino.packages.uploaders.HttpUploader; +import cc.arduino.packages.uploaders.SSHUploader; import cc.arduino.packages.uploaders.SerialUploader; +import org.junit.Before; +import org.junit.Test; import processing.app.AbstractWithPreferencesTest; import java.io.File; @@ -23,15 +22,15 @@ public class UploaderFactoryTest extends AbstractWithPreferencesTest { } @Test - public void shouldCreateAnInstanceOfHttpUploader() throws Exception { + public void shouldCreateAnInstanceOfSSHUploader() throws Exception { TargetBoard board = targetPackage.getPlatforms().get("avr").getBoards().get("yun"); Uploader uploader = new UploaderFactory().newUploader(board, "192.168.0.1 (yun)"); - assertTrue(uploader instanceof HttpUploader); + assertTrue(uploader instanceof SSHUploader); } @Test - public void shouldCreateAnInstanceOfBasicUploaderWhenHTTPIsUnsupported() throws Exception { + public void shouldCreateAnInstanceOfBasicUploaderWhenSSHIsUnsupported() throws Exception { TargetBoard board = targetPackage.getPlatforms().get("avr").getBoards().get("uno"); Uploader uploader = new UploaderFactory().newUploader(board, "192.168.0.1 (myyun)"); diff --git a/hardware/arduino/avr/boards.txt b/hardware/arduino/avr/boards.txt index a300a5d88..3f5db4d86 100644 --- a/hardware/arduino/avr/boards.txt +++ b/hardware/arduino/avr/boards.txt @@ -5,7 +5,7 @@ menu.cpu=Processor ############################################################## yun.name=Arduino Yún -yun.upload.via_http=true +yun.upload.via_ssh=true yun.vid.0=0x2341 yun.pid.0=0x0041