001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.ruby.railsprojects.server;
042:
043: import java.awt.Component;
044: import java.awt.event.ActionEvent;
045: import java.io.BufferedReader;
046: import java.io.File;
047: import java.io.IOException;
048: import java.io.InputStreamReader;
049: import java.io.PrintWriter;
050: import java.net.InetSocketAddress;
051: import java.net.MalformedURLException;
052: import java.net.Socket;
053: import java.net.URL;
054: import java.util.ArrayList;
055: import java.util.HashSet;
056: import java.util.List;
057: import java.util.Set;
058: import java.util.concurrent.ExecutionException;
059: import java.util.concurrent.Future;
060: import java.util.concurrent.Semaphore;
061: import java.util.logging.Level;
062: import java.util.logging.Logger;
063: import javax.swing.AbstractAction;
064: import javax.swing.AbstractListModel;
065: import javax.swing.ComboBoxModel;
066: import javax.swing.JComboBox;
067: import javax.swing.JLabel;
068: import javax.swing.JList;
069: import javax.swing.ListCellRenderer;
070: import org.netbeans.modules.ruby.platform.execution.DirectoryFileLocator;
071: import org.netbeans.api.progress.ProgressHandle;
072: import org.netbeans.api.progress.ProgressHandleFactory;
073: import org.netbeans.modules.ruby.platform.execution.ExecutionDescriptor;
074: import org.netbeans.modules.ruby.platform.execution.OutputRecognizer;
075: import org.openide.DialogDisplayer;
076: import org.openide.awt.HtmlBrowser;
077: import org.openide.awt.StatusDisplayer;
078: import org.openide.filesystems.FileUtil;
079: import org.openide.util.Cancellable;
080: import org.openide.util.Exceptions;
081: import org.openide.util.NbBundle;
082: import org.openide.util.RequestProcessor;
083: import org.netbeans.api.project.ProjectInformation;
084: import org.netbeans.api.ruby.platform.RubyPlatform;
085: import org.netbeans.modules.ruby.platform.RubyExecution;
086: import org.netbeans.modules.ruby.railsprojects.RailsProject;
087: import org.netbeans.modules.ruby.railsprojects.server.spi.RubyInstance;
088: import org.netbeans.modules.ruby.railsprojects.ui.customizer.RailsProjectProperties;
089: import org.openide.ErrorManager;
090: import org.openide.NotifyDescriptor;
091:
092: /**
093: * Support for the builtin Ruby on Rails web server: WEBrick, Mongrel, Lighttpd
094: *
095: * This is really primitive at this point; I should talk to the people who
096: * write Java web server plugins and take some pointers. Perhaps it can
097: * even implement some of their APIs such that logging, runtime nodes etc.
098: * all begin to work.
099: *
100: * @todo When launching under JRuby, also pass in -Djruby.thread.pooling=true to the VM
101: * @todo Rewrite the WEBrick error message which says to press Ctrl-C to cancel the process;
102: * tell the user to use the Stop button in the margin instead (somebody on nbusers asked about this)
103: *
104: * @author Tor Norbye, Pavel Buzek, Erno Mononen
105: */
106: public final class RailsServerManager {
107:
108: enum ServerStatus {
109: NOT_STARTED, STARTING, RUNNING;
110: }
111:
112: private static final Logger LOGGER = Logger
113: .getLogger(RailsServerManager.class.getName());
114:
115: /** Set of currently active - in use; ports. */
116: private static final Set<Integer> IN_USE_PORTS = new HashSet<Integer>();;
117:
118: /**
119: * The timeout in seconds for waiting a server to start.
120: */
121: private static final int SERVER_STARTUP_TIMEOUT = 120;
122:
123: private ServerStatus status = ServerStatus.NOT_STARTED;
124: private RubyServer server;
125:
126: /** True if server failed to start due to port conflict. */
127: private boolean portConflict;
128:
129: /** User chosen port */
130: private int originalPort;
131:
132: /** Actual port in use (trying other ports for ones not in use) */
133: private int port = -1;
134:
135: private RailsProject project;
136: private RubyExecution execution;
137: private File dir;
138: private boolean debug;
139: private boolean switchToDebugMode;
140: private Semaphore debugSemaphore;
141:
142: public RailsServerManager(RailsProject project) {
143: this .project = project;
144: dir = FileUtil.toFile(project.getProjectDirectory());
145: }
146:
147: public synchronized void setDebug(boolean debug) {
148: if (status == ServerStatus.RUNNING && !this .debug && debug) {
149: switchToDebugMode = true;
150: }
151: this .debug = debug;
152: }
153:
154: private void ensureRunning() {
155: synchronized (RailsServerManager.this ) {
156: if (status == ServerStatus.STARTING) {
157: return;
158: } else if (status == ServerStatus.RUNNING) {
159: if (switchToDebugMode) {
160: assert debugSemaphore == null : "startSemaphor supposed to be null";
161: debugSemaphore = new Semaphore(0);
162: switchToDebugMode = false;
163: } else if (isPortInUse(port)) {
164: // Simply assume it is still the same server running
165: return;
166: }
167: }
168: }
169: if (debugSemaphore != null) {
170: try {
171: execution.kill();
172: debugSemaphore.acquire();
173: debugSemaphore = null;
174: } catch (InterruptedException ex) {
175: Exceptions.printStackTrace(ex);
176: }
177: }
178:
179: // Server was not started or was killed externally
180: Runnable finishedAction = new Runnable() {
181: public void run() {
182: synchronized (RailsServerManager.this ) {
183: status = ServerStatus.NOT_STARTED;
184: if (server != null) {
185: server.removeApplication(port);
186: }
187: IN_USE_PORTS.remove(port);
188: if (portConflict) {
189: // Failed to start due to port conflict - notify user.
190: notifyPortConflict();
191: }
192: if (debugSemaphore != null) {
193: debugSemaphore.release();
194: } else {
195: debug = false;
196: }
197: }
198: }
199: };
200:
201: // Start the server
202: synchronized (RailsServerManager.this ) {
203: status = ServerStatus.STARTING;
204: }
205:
206: portConflict = false;
207: String portString = project.evaluator().getProperty(
208: RailsProjectProperties.RAILS_PORT);
209: port = 0;
210: if (portString != null) {
211: port = Integer.parseInt(portString);
212: }
213: if (port == 0) {
214: port = 3000;
215: }
216: originalPort = port;
217:
218: while (isPortInUse(port)) {
219: port++;
220: }
221: String projectName = project.getLookup().lookup(
222: ProjectInformation.class).getDisplayName();
223: String classPath = project.evaluator().getProperty(
224: RailsProjectProperties.JAVAC_CLASSPATH);
225: String serverId = project.evaluator().getProperty(
226: RailsProjectProperties.RAILS_SERVERTYPE);
227: RubyPlatform platform = RubyPlatform.platformFor(project);
228: RubyInstance instance = ServerRegistry.getDefault().getServer(
229: serverId, platform);
230: if (instance == null) {
231: // TODO: need to inform the user somehow
232: // fall back to the first available server
233: List<? extends RubyInstance> availableServers = ServerRegistry
234: .getDefault().getServers();
235: for (RubyInstance each : availableServers) {
236: if (each.isPlatformSupported(platform)) {
237: instance = each;
238: break;
239: }
240: }
241: assert instance != null : "No servers found for "
242: + platform;
243: }
244: if (!(instance instanceof RubyServer)) {
245: final Future<RubyInstance.OperationState> result = instance
246: .runApplication(platform, projectName, dir);
247:
248: final RubyInstance serverInstance = instance;
249: RequestProcessor.getDefault().post(new Runnable() {
250: public void run() {
251: try {
252: RubyInstance.OperationState state = result
253: .get();
254: if (state == RubyInstance.OperationState.COMPLETED) {
255: synchronized (RailsServerManager.this ) {
256: port = serverInstance.getRailsPort();
257: status = ServerStatus.RUNNING;
258: }
259: } else {
260: synchronized (RailsServerManager.this ) {
261: status = ServerStatus.NOT_STARTED;
262: }
263: }
264: } catch (Exception ex) {
265: LOGGER.log(Level.INFO, ex.getMessage(), ex);
266:
267: // Ensure status value is reset on exceptions too...
268: synchronized (RailsServerManager.this ) {
269: status = ServerStatus.NOT_STARTED;
270: }
271: }
272: }
273: });
274:
275: return;
276: }
277: server = (RubyServer) instance;
278: String displayName = getServerTabName(server, projectName, port);
279: String serverPath = server.getServerPath();
280: ExecutionDescriptor desc = new ExecutionDescriptor(RubyPlatform
281: .platformFor(project), displayName, dir, serverPath);
282: desc.additionalArgs(buildStartupArgs());
283: desc.postBuild(finishedAction);
284: desc.classPath(classPath);
285: desc.addStandardRecognizers();
286: desc.addOutputRecognizer(new RailsServerRecognizer(server));
287: desc.frontWindow(false);
288: desc.debug(debug);
289: desc.fastDebugRequired(debug);
290: desc.fileLocator(new DirectoryFileLocator(FileUtil
291: .toFileObject(dir)));
292: //desc.showProgress(false); // http://ruby.netbeans.org/issues/show_bug.cgi?id=109261
293: desc.showSuspended(true);
294: String charsetName = project.evaluator().getProperty(
295: RailsProjectProperties.SOURCE_ENCODING);
296: IN_USE_PORTS.add(port);
297: execution = new RubyExecution(desc, charsetName);
298: execution.run();
299: }
300:
301: private String[] buildStartupArgs() {
302: List<String> result = new ArrayList<String>();
303: if (server.getStartupParam() != null) {
304: result.add(server.getStartupParam());
305: }
306: String railsEnv = project.evaluator().getProperty(
307: RailsProjectProperties.RAILS_ENV);
308: if (railsEnv != null && !"".equals(railsEnv.trim())) {
309: result.add("-e");
310: result.add(railsEnv);
311: }
312: result.add("--port");
313: result.add(Integer.toString(port));
314: return result.toArray(new String[result.size()]);
315: }
316:
317: private static String getServerTabName(RubyServer server,
318: String projectName, int port) {
319: return NbBundle.getMessage(RailsServerManager.class,
320: "LBL_ServerTab", server.getDisplayName(), projectName,
321: String.valueOf(port));
322: }
323:
324: private void notifyPortConflict() {
325: String message = NbBundle.getMessage(RailsServerManager.class,
326: "Conflict", Integer.toString(originalPort));
327: NotifyDescriptor nd = new NotifyDescriptor.Message(message,
328: NotifyDescriptor.Message.ERROR_MESSAGE);
329: DialogDisplayer.getDefault().notify(nd);
330: }
331:
332: /**
333: * Starts the server if not running and shows url.
334: * @param relativeUrl the resulting url will be for example: http://localhost:{port}/{relativeUrl}
335: */
336: public void showUrl(final String relativeUrl) {
337: synchronized (RailsServerManager.this ) {
338: if (!switchToDebugMode && status == ServerStatus.RUNNING
339: && isPortInUse(port)) {
340: RailsServerManager.showURL(relativeUrl, port);
341: return;
342: }
343: }
344: ensureRunning();
345:
346: String displayName = NbBundle.getMessage(
347: RailsServerManager.class, "ServerStartup");
348: final ProgressHandle handle = ProgressHandleFactory
349: .createHandle(displayName, new Cancellable() {
350: public boolean cancel() {
351: return true;
352: }
353: }, new AbstractAction() {
354: public void actionPerformed(ActionEvent e) {
355: // XXX ?
356: }
357: });
358:
359: handle.start();
360: handle.switchToIndeterminate();
361:
362: RequestProcessor.getDefault().post(new Runnable() {
363: public void run() {
364: try {
365: // Try connecting repeatedly, up to time specified
366: // by SERVER_STARTUP_TIMEOUT, then bail
367: int i = 0;
368: for (; i <= SERVER_STARTUP_TIMEOUT; i++) {
369: try {
370: Thread.sleep(1000);
371: } catch (InterruptedException ie) {
372: // Don't worry about it
373: }
374:
375: synchronized (RailsServerManager.this ) {
376: if (status == ServerStatus.RUNNING) {
377: LOGGER.fine("Server " + server
378: + " started in " + i
379: + " seconds.");
380: RailsServerManager.showURL(relativeUrl,
381: port);
382: return;
383: }
384:
385: if (status == ServerStatus.NOT_STARTED) {
386: LOGGER
387: .fine("Server starup failed, server type is: "
388: + server);
389: // Server startup somehow failed...
390: break;
391: }
392: }
393: }
394:
395: LOGGER.fine("Could not start " + server + " in "
396: + i + " seconds, current server status is "
397: + status);
398:
399: StatusDisplayer.getDefault().setStatusText(
400: NbBundle.getMessage(
401: RailsServerManager.class,
402: "NoServerFound",
403: "http://localhost:" + port + "/"
404: + relativeUrl));
405: } finally {
406: handle.finish();
407: }
408: }
409: });
410: }
411:
412: /** Return true if there is an HTTP response from the port on localhost.
413: * Based on tomcatint\tomcat5\src\org.netbeans.modules.tomcat5.util.Utils.java.
414: */
415: public static boolean isPortInUse(int port) {
416: if (IN_USE_PORTS.contains(port)) {
417: return true;
418: }
419: int timeout = 3000;
420: Socket socket = new Socket();
421: try {
422: try {
423: socket.connect(
424: new InetSocketAddress("localhost", port),
425: timeout); // NOI18N
426: socket.setSoTimeout(timeout);
427: PrintWriter out = new PrintWriter(socket
428: .getOutputStream(), true);
429: try {
430: BufferedReader in = new BufferedReader(
431: new InputStreamReader(socket
432: .getInputStream()));
433: try {
434: // request
435: out.println("GET /\n"); // NOI18N
436:
437: // response
438: String text = in.readLine();
439: if (text == null
440: || !text.startsWith("<!DOCTYPE")) { // NOI18N
441: return false; // not an http response
442: }
443: return true;
444: } finally {
445: in.close();
446: }
447: } finally {
448: out.close();
449: }
450: } finally {
451: socket.close();
452: }
453: } catch (IOException ioe) {
454: return false;
455: }
456: }
457:
458: private static void showURL(String relativeUrl, int port) {
459: LOGGER.fine("Opening URL: " + "http://localhost:" + port + "/"
460: + relativeUrl);
461: try {
462: URL url = new URL("http://localhost:" + port + "/"
463: + relativeUrl); // NOI18N
464: HtmlBrowser.URLDisplayer.getDefault().showURL(url);
465: } catch (MalformedURLException ex) {
466: ErrorManager.getDefault().notify(ex);
467: }
468: }
469:
470: /**
471: * @param outputLine the output line to check.
472: * @return true if the given <code>outputLine</code> represented 'address in use'
473: * message.
474: */
475: static boolean isAddressInUseMsg(String outputLine) {
476: return outputLine
477: .matches(".*in.*: Address.+in use.+(Errno::EADDRINUSE).*"); //NOI18N
478: }
479:
480: private class RailsServerRecognizer extends OutputRecognizer {
481:
482: private final RubyServer server;
483:
484: RailsServerRecognizer(RubyServer server) {
485: this .server = server;
486: }
487:
488: @Override
489: public ActionText processLine(String outputLine) {
490:
491: if (LOGGER.isLoggable(Level.FINEST)) {
492: LOGGER.log(Level.FINEST, "Processing output line: "
493: + outputLine);
494: }
495:
496: String line = outputLine;
497:
498: // This is ugly, but my attempts to use URLConnection on the URL repeatedly
499: // and check for connection.getResponseCode()==HttpURLConnection.HTTP_OK didn't
500: // work - try that again later
501: if (server.isStartupMsg(outputLine)) {
502: synchronized (RailsServerManager.this ) {
503: LOGGER.fine("Identified " + server + " as running");
504: status = ServerStatus.RUNNING;
505: String projectName = project.getLookup().lookup(
506: ProjectInformation.class).getDisplayName();
507: server.addApplication(new RailsApplication(
508: projectName, port, execution));
509: }
510: } else if (isAddressInUseMsg(outputLine)) {
511: LOGGER.fine("Detected port conflict: " + outputLine);
512: portConflict = true;
513: }
514:
515: if (!line.equals(outputLine)) {
516: return new ActionText(new String[] { line }, null,
517: null, null);
518: }
519:
520: return null;
521: }
522: }
523:
524: public static JComboBox getServerComboBox(RubyPlatform platform) {
525: JComboBox result = new JComboBox();
526: if (platform != null) {
527: result.setModel(new ServerListModel(platform));
528: }
529: result.setRenderer(new ServerListCellRendered());
530: return result;
531: }
532:
533: public static class ServerListModel extends AbstractListModel
534: implements ComboBoxModel {
535:
536: private final List<? extends RubyInstance> servers;
537: private Object selected;
538:
539: public ServerListModel(RubyPlatform platform) {
540: this .servers = ServerRegistry.getDefault().getServers(
541: platform);
542: this .selected = servers.get(0);
543: }
544:
545: public int getSize() {
546: return servers.size();
547: }
548:
549: public Object getElementAt(int index) {
550: return servers.get(index);
551: }
552:
553: public void setSelectedItem(Object server) {
554: if (selected != server) {
555: this .selected = server;
556: fireContentsChanged(this , -1, -1);
557: }
558: }
559:
560: public Object getSelectedItem() {
561: return selected;
562: }
563:
564: }
565:
566: private static class ServerListCellRendered extends JLabel
567: implements ListCellRenderer {
568:
569: public ServerListCellRendered() {
570: setOpaque(true);
571: }
572:
573: public Component getListCellRendererComponent(JList list,
574: Object value, int index, boolean isSelected,
575: boolean cellHasFocus) {
576: RubyInstance server = (RubyInstance) value;
577: if (server != null) {
578: setText(server.getDisplayName());
579: setForeground(isSelected ? list
580: .getSelectionForeground() : list
581: .getForeground());
582: setBackground(isSelected ? list
583: .getSelectionBackground() : list
584: .getBackground());
585: }
586: return this;
587: }
588: }
589:
590: }
|