PageRenderTime 433ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/remoting/src/main/java/hudson/remoting/Engine.java

https://github.com/sikakura/jenkins
Java | 369 lines | 252 code | 41 blank | 76 comment | 35 complexity | 6ce9b5fb421ceb10f1dd123d29e52961 MD5 | raw file
Possible License(s): MIT, BSD-3-Clause
  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. */
  24. package hudson.remoting;
  25. import java.io.BufferedInputStream;
  26. import java.io.BufferedOutputStream;
  27. import java.io.DataOutputStream;
  28. import java.io.IOException;
  29. import java.io.InputStream;
  30. import java.io.ByteArrayOutputStream;
  31. import java.net.HttpURLConnection;
  32. import java.net.Socket;
  33. import java.net.URL;
  34. import java.util.Properties;
  35. import java.util.concurrent.ExecutorService;
  36. import java.util.concurrent.Executors;
  37. import java.util.concurrent.ThreadFactory;
  38. import java.util.List;
  39. import java.util.Collections;
  40. import java.util.logging.Logger;
  41. import static java.util.logging.Level.SEVERE;
  42. /**
  43. * Slave agent engine that proactively connects to Hudson master.
  44. *
  45. * @author Kohsuke Kawaguchi
  46. */
  47. public class Engine extends Thread {
  48. /**
  49. * Thread pool that sets {@link #CURRENT}.
  50. */
  51. private final ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactory() {
  52. private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
  53. public Thread newThread(final Runnable r) {
  54. return defaultFactory.newThread(new Runnable() {
  55. public void run() {
  56. CURRENT.set(Engine.this);
  57. r.run();
  58. }
  59. });
  60. }
  61. });
  62. public final EngineListener listener;
  63. /**
  64. * To make Hudson more graceful against user error,
  65. * JNLP agent can try to connect to multiple possible Hudson URLs.
  66. * This field specifies those candidate URLs, such as
  67. * "http://foo.bar/hudson/".
  68. */
  69. private List<URL> candidateUrls;
  70. /**
  71. * URL that points to Hudson's tcp slage agent listener, like <tt>http://myhost/hudson/</tt>
  72. *
  73. * <p>
  74. * This value is determined from {@link #candidateUrls} after a successful connection.
  75. * Note that this URL <b>DOES NOT</b> have "tcpSlaveAgentListener" in it.
  76. */
  77. private URL hudsonUrl;
  78. private final String secretKey;
  79. public final String slaveName;
  80. private String credentials;
  81. /**
  82. * See Main#tunnel in the jnlp-agent module for the details.
  83. */
  84. private String tunnel;
  85. private boolean noReconnect;
  86. /**
  87. * This cookie identifiesof the current connection, allowing us to force the server to drop
  88. * the client if we initiate a reconnection from our end (even when the server still thinks
  89. * the connection is alive.)
  90. */
  91. private String cookie;
  92. public Engine(EngineListener listener, List<URL> hudsonUrls, String secretKey, String slaveName) {
  93. this.listener = listener;
  94. this.candidateUrls = hudsonUrls;
  95. this.secretKey = secretKey;
  96. this.slaveName = slaveName;
  97. if(candidateUrls.isEmpty())
  98. throw new IllegalArgumentException("No URLs given");
  99. }
  100. public URL getHudsonUrl() {
  101. return hudsonUrl;
  102. }
  103. public void setTunnel(String tunnel) {
  104. this.tunnel = tunnel;
  105. }
  106. public void setCredentials(String creds) {
  107. this.credentials = creds;
  108. }
  109. public void setNoReconnect(boolean noReconnect) {
  110. this.noReconnect = noReconnect;
  111. }
  112. @SuppressWarnings({"ThrowableInstanceNeverThrown"})
  113. @Override
  114. public void run() {
  115. try {
  116. boolean first = true;
  117. while(true) {
  118. if(first) {
  119. first = false;
  120. } else {
  121. if(noReconnect)
  122. return; // exit
  123. }
  124. listener.status("Locating server among " + candidateUrls);
  125. Throwable firstError=null;
  126. String port=null;
  127. for (URL url : candidateUrls) {
  128. String s = url.toExternalForm();
  129. if(!s.endsWith("/")) s+='/';
  130. URL salURL = new URL(s+"tcpSlaveAgentListener/");
  131. // find out the TCP port
  132. HttpURLConnection con = (HttpURLConnection)salURL.openConnection();
  133. if (con instanceof HttpURLConnection && credentials != null) {
  134. String encoding = new sun.misc.BASE64Encoder().encode(credentials.getBytes());
  135. con.setRequestProperty("Authorization", "Basic " + encoding);
  136. }
  137. try {
  138. try {
  139. con.setConnectTimeout(30000);
  140. con.setReadTimeout(60000);
  141. con.connect();
  142. } catch (IOException x) {
  143. if (firstError == null) {
  144. firstError = new IOException("Failed to connect to " + salURL + ": " + x.getMessage()).initCause(x);
  145. }
  146. continue;
  147. }
  148. port = con.getHeaderField("X-Hudson-JNLP-Port");
  149. if(con.getResponseCode()!=200) {
  150. if(firstError==null)
  151. firstError = new Exception(salURL+" is invalid: "+con.getResponseCode()+" "+con.getResponseMessage());
  152. continue;
  153. }
  154. if(port ==null) {
  155. if(firstError==null)
  156. firstError = new Exception(url+" is not Hudson");
  157. continue;
  158. }
  159. } finally {
  160. con.disconnect();
  161. }
  162. // this URL works. From now on, only try this URL
  163. hudsonUrl = url;
  164. firstError = null;
  165. candidateUrls = Collections.singletonList(hudsonUrl);
  166. break;
  167. }
  168. if(firstError!=null) {
  169. listener.error(firstError);
  170. return;
  171. }
  172. Socket s = connect(port);
  173. listener.status("Handshaking");
  174. DataOutputStream dos = new DataOutputStream(s.getOutputStream());
  175. BufferedInputStream in = new BufferedInputStream(s.getInputStream());
  176. dos.writeUTF("Protocol:JNLP2-connect");
  177. Properties props = new Properties();
  178. props.put("Secret-Key", secretKey);
  179. props.put("Node-Name", slaveName);
  180. if (cookie!=null)
  181. props.put("Cookie", cookie);
  182. ByteArrayOutputStream o = new ByteArrayOutputStream();
  183. props.store(o, null);
  184. dos.writeUTF(o.toString("UTF-8"));
  185. String greeting = readLine(in);
  186. if (greeting.startsWith("Unknown protocol")) {
  187. LOGGER.info("The server didn't understand the v2 handshake. Falling back to v1 handshake");
  188. s.close();
  189. s = connect(port);
  190. in = new BufferedInputStream(s.getInputStream());
  191. dos = new DataOutputStream(s.getOutputStream());
  192. dos.writeUTF("Protocol:JNLP-connect");
  193. dos.writeUTF(secretKey);
  194. dos.writeUTF(slaveName);
  195. greeting = readLine(in); // why, oh why didn't I use DataOutputStream when writing to the network?
  196. if (!greeting.equals(GREETING_SUCCESS)) {
  197. onConnectionRejected(greeting);
  198. continue;
  199. }
  200. } else {
  201. if (greeting.equals(GREETING_SUCCESS)) {
  202. Properties responses = readResponseHeaders(in);
  203. cookie = responses.getProperty("Cookie");
  204. } else {
  205. onConnectionRejected(greeting);
  206. continue;
  207. }
  208. }
  209. final Socket socket = s;
  210. final Channel channel = new Channel("channel", executor,
  211. in,
  212. new BufferedOutputStream(s.getOutputStream()));
  213. PingThread t = new PingThread(channel) {
  214. protected void onDead() {
  215. try {
  216. if (!channel.isInClosed()) {
  217. LOGGER.info("Ping failed. Terminating the socket.");
  218. socket.close();
  219. }
  220. } catch (IOException e) {
  221. LOGGER.log(SEVERE, "Failed to terminate the socket", e);
  222. }
  223. }
  224. };
  225. t.start();
  226. listener.status("Connected");
  227. channel.join();
  228. listener.status("Terminated");
  229. t.interrupt(); // make sure the ping thread is terminated
  230. listener.onDisconnect();
  231. if(noReconnect)
  232. return; // exit
  233. // try to connect back to the server every 10 secs.
  234. waitForServerToBack();
  235. }
  236. } catch (Throwable e) {
  237. listener.error(e);
  238. }
  239. }
  240. private void onConnectionRejected(String greeting) throws InterruptedException {
  241. listener.error(new Exception("The server rejected the connection: "+greeting));
  242. Thread.sleep(10*1000);
  243. }
  244. private Properties readResponseHeaders(BufferedInputStream in) throws IOException {
  245. Properties response = new Properties();
  246. while (true) {
  247. String line = readLine(in);
  248. if (line.length()==0)
  249. return response;
  250. int idx = line.indexOf(':');
  251. response.put(line.substring(0,idx).trim(), line.substring(idx+1).trim());
  252. }
  253. }
  254. /**
  255. * Read until '\n' and returns it as a string.
  256. */
  257. private static String readLine(InputStream in) throws IOException {
  258. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  259. while (true) {
  260. int ch = in.read();
  261. if (ch<0 || ch=='\n')
  262. return baos.toString().trim(); // trim off possible '\r'
  263. baos.write(ch);
  264. }
  265. }
  266. /**
  267. * Connects to TCP slave port, with a few retries.
  268. */
  269. private Socket connect(String port) throws IOException, InterruptedException {
  270. String host = this.hudsonUrl.getHost();
  271. if(tunnel!=null) {
  272. String[] tokens = tunnel.split(":",3);
  273. if(tokens.length!=2)
  274. throw new IOException("Illegal tunneling parameter: "+tunnel);
  275. if(tokens[0].length()>0) host = tokens[0];
  276. if(tokens[1].length()>0) port = tokens[1];
  277. }
  278. String msg = "Connecting to " + host + ':' + port;
  279. listener.status(msg);
  280. int retry = 1;
  281. while(true) {
  282. try {
  283. Socket s = new Socket(host, Integer.parseInt(port));
  284. s.setTcpNoDelay(true); // we'll do buffering by ourselves
  285. // set read time out to avoid infinite hang. the time out should be long enough so as not
  286. // to interfere with normal operation. the main purpose of this is that when the other peer dies
  287. // abruptly, we shouldn't hang forever, and at some point we should notice that the connection
  288. // is gone.
  289. s.setSoTimeout(30*60*1000); // 30 mins. See PingThread for the ping interval
  290. return s;
  291. } catch (IOException e) {
  292. if(retry++>10)
  293. throw (IOException)new IOException("Failed to connect to "+host+':'+port).initCause(e);
  294. Thread.sleep(1000*10);
  295. listener.status(msg+" (retrying:"+retry+")",e);
  296. }
  297. }
  298. }
  299. /**
  300. * Waits for the server to come back.
  301. */
  302. private void waitForServerToBack() throws InterruptedException {
  303. while(true) {
  304. Thread.sleep(1000*10);
  305. try {
  306. // Hudson top page might be read-protected. see http://www.nabble.com/more-lenient-retry-logic-in-Engine.waitForServerToBack-td24703172.html
  307. HttpURLConnection con = (HttpURLConnection)new URL(hudsonUrl,"tcpSlaveAgentListener/").openConnection();
  308. con.connect();
  309. if(con.getResponseCode()==200)
  310. return;
  311. } catch (IOException e) {
  312. // retry
  313. }
  314. }
  315. }
  316. /**
  317. * When invoked from within remoted {@link Callable} (that is,
  318. * from the thread that carries out the remote requests),
  319. * this method returns the {@link Engine} in which the remote operations
  320. * run.
  321. */
  322. public static Engine current() {
  323. return CURRENT.get();
  324. }
  325. private static final ThreadLocal<Engine> CURRENT = new ThreadLocal<Engine>();
  326. private static final Logger LOGGER = Logger.getLogger(Engine.class.getName());
  327. public static final String GREETING_SUCCESS = "Welcome";
  328. }