diff options
Diffstat (limited to 'subsonic-booter/src')
26 files changed, 1922 insertions, 0 deletions
diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/Main.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/Main.java new file mode 100644 index 00000000..b7879e12 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/Main.java @@ -0,0 +1,65 @@ +package net.sourceforge.subsonic.booter; + +import java.util.Arrays; +import java.util.List; + +import javax.swing.JOptionPane; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import net.sourceforge.subsonic.booter.agent.SettingsPanel; +import net.sourceforge.subsonic.booter.agent.SubsonicAgent; + +/** + * Application entry point for Subsonic booter. + * <p/> + * Use command line argument "-agent" to start the Windows service monitoring agent, + * or "-mac" to start the Mac version of the deployer. + * + * @author Sindre Mehus + */ +public class Main { + + public Main(String contextName, List<String> args) { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext" + contextName + ".xml"); + + if ("-agent".equals(contextName)) { + + SubsonicAgent agent = (SubsonicAgent) context.getBean("agent"); + SettingsPanel settingsPanel = (SettingsPanel) context.getBean("settingsPanel"); + + agent.setElevated(args.contains("-elevated")); + + if (args.contains("-balloon")) { + agent.showTrayIconMessage(); + } + + if (args.contains("-stop")) { + agent.startOrStopService(false); + agent.showStatusPanel(); + } else if (args.contains("-start")) { + agent.startOrStopService(true); + agent.showStatusPanel(); + } + + if (args.contains("-settings")) { + String[] settings = args.get(args.indexOf("-settings") + 1).split(","); + try { + agent.showSettingsPanel(); + settingsPanel.saveSettings(Integer.valueOf(settings[0]), Integer.valueOf(settings[1]), Integer.valueOf(settings[2]), settings[3]); + settingsPanel.readValues(); + } catch (Exception x) { + JOptionPane.showMessageDialog(settingsPanel, x.getMessage(), "Error", JOptionPane.WARNING_MESSAGE); + } + } + } + } + + public static void main(String[] args) { + String context = "-deployer"; + if (args.length > 0) { + context = args[0]; + } + new Main(context, Arrays.asList(args)); + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SettingsPanel.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SettingsPanel.java new file mode 100644 index 00000000..c225410f --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SettingsPanel.java @@ -0,0 +1,375 @@ +package net.sourceforge.subsonic.booter.agent; + +import com.jgoodies.forms.builder.DefaultFormBuilder; +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.factories.ButtonBarFactory; +import com.jgoodies.forms.layout.FormLayout; +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployer; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.Format; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Panel displaying the settings of the Subsonic service. + * + * @author Sindre Mehus + */ +public class SettingsPanel extends JPanel implements SubsonicListener { + + private static final Format INTEGER_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.UK)); + + private final SubsonicAgent subsonicAgent; + private JFormattedTextField portTextField; + private JCheckBox httpsPortCheckBox; + private JFormattedTextField httpsPortTextField; + private JComboBox contextPathComboBox; + private JFormattedTextField memoryTextField; + private JButton defaultButton; + private JButton saveButton; + public SettingsPanel(SubsonicAgent subsonicAgent) { + this.subsonicAgent = subsonicAgent; + createComponents(); + configureComponents(); + layoutComponents(); + addBehaviour(); + readValues(); + subsonicAgent.addListener(this); + } + + public void readValues() { + portTextField.setValue(getPortFromOptionsFile()); + memoryTextField.setValue(getMemoryLimitFromOptionsFile()); + contextPathComboBox.setSelectedItem(getContextPathFromOptionsFile()); + int httpsPort = getHttpsPortFromOptionsFile(); + boolean httpsEnabled = httpsPort != 0; + httpsPortTextField.setValue(httpsEnabled ? httpsPort : 4443); + httpsPortTextField.setEnabled(httpsEnabled); + httpsPortCheckBox.setSelected(httpsEnabled); + } + + private int getHttpsPortFromOptionsFile() { + try { + String s = grep("-Dsubsonic.httpsPort=(\\d+)"); + return Integer.parseInt(s); + } catch (Exception x) { + x.printStackTrace(); + return SubsonicDeployer.DEFAULT_HTTPS_PORT; + } + } + + private int getPortFromOptionsFile() { + try { + String s = grep("-Dsubsonic.port=(\\d+)"); + return Integer.parseInt(s); + } catch (Exception x) { + x.printStackTrace(); + return SubsonicDeployer.DEFAULT_PORT; + } + } + + private int getMemoryLimitFromOptionsFile() { + try { + String s = grep("-Xmx(\\d+)m"); + return Integer.parseInt(s); + } catch (Exception x) { + x.printStackTrace(); + return SubsonicDeployer.DEFAULT_MEMORY_LIMIT; + } + } + + private String getContextPathFromOptionsFile() { + try { + String s = grep("-Dsubsonic.contextPath=(.*)"); + if (s == null) { + throw new NullPointerException(); + } + return s; + } catch (Exception x) { + x.printStackTrace(); + return SubsonicDeployer.DEFAULT_CONTEXT_PATH; + } + } + + private void createComponents() { + portTextField = new JFormattedTextField(INTEGER_FORMAT); + httpsPortTextField = new JFormattedTextField(INTEGER_FORMAT); + httpsPortCheckBox = new JCheckBox("Enable https on port"); + contextPathComboBox = new JComboBox(); + memoryTextField = new JFormattedTextField(INTEGER_FORMAT); + defaultButton = new JButton("Restore defaults"); + saveButton = new JButton("Save settings"); + } + + private void configureComponents() { + contextPathComboBox.setEditable(true); + contextPathComboBox.addItem("/"); + contextPathComboBox.addItem("/subsonic"); + contextPathComboBox.addItem("/music"); + } + + private void layoutComponents() { + FormLayout layout = new FormLayout("d, 6dlu, max(d;30dlu):grow"); + DefaultFormBuilder builder = new DefaultFormBuilder(layout); + builder.append("Port number", portTextField); + builder.append(httpsPortCheckBox, httpsPortTextField); + builder.append("Memory limit (MB)", memoryTextField); + builder.append("Context path", contextPathComboBox); + + setBorder(Borders.DIALOG_BORDER); + + setLayout(new BorderLayout(12, 12)); + add(builder.getPanel(), BorderLayout.CENTER); + add(ButtonBarFactory.buildCenteredBar(defaultButton, saveButton), BorderLayout.SOUTH); + } + + private void addBehaviour() { + saveButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + try { + subsonicAgent.checkElevation("-settings", getMemoryLimit() + "," + getPort() + "," + getHttpsPort() + "," + getContextPath()); + saveSettings(getMemoryLimit(), getPort(), getHttpsPort(), getContextPath()); + } catch (Exception x) { + JOptionPane.showMessageDialog(SettingsPanel.this, x.getMessage(), "Error", JOptionPane.WARNING_MESSAGE); + } + } + }); + + defaultButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + portTextField.setValue(SubsonicDeployer.DEFAULT_PORT); + httpsPortTextField.setValue(4443); + httpsPortTextField.setEnabled(false); + httpsPortCheckBox.setSelected(false); + memoryTextField.setValue(SubsonicDeployer.DEFAULT_MEMORY_LIMIT); + contextPathComboBox.setSelectedItem(SubsonicDeployer.DEFAULT_CONTEXT_PATH); + } + }); + + httpsPortCheckBox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + httpsPortTextField.setEnabled(httpsPortCheckBox.isSelected()); + } + }); + } + + private String getContextPath() throws SettingsException { + String contextPath = (String) contextPathComboBox.getSelectedItem(); + if (contextPath.contains(" ") || !contextPath.startsWith("/")) { + throw new SettingsException("Please specify a valid context path."); + } + return contextPath; + } + + private int getMemoryLimit() throws SettingsException { + int memoryLimit; + try { + memoryLimit = ((Number) memoryTextField.getValue()).intValue(); + if (memoryLimit < 5) { + throw new Exception(); + } + } catch (Exception x) { + throw new SettingsException("Please specify a valid memory limit.", x); + } + return memoryLimit; + } + + private int getPort() throws SettingsException { + int port; + try { + port = ((Number) portTextField.getValue()).intValue(); + if (port < 1 || port > 65535) { + throw new Exception(); + } + } catch (Exception x) { + throw new SettingsException("Please specify a valid port number.", x); + } + return port; + } + + private int getHttpsPort() throws SettingsException { + if (!httpsPortCheckBox.isSelected()) { + return 0; + } + + int port; + try { + port = ((Number) httpsPortTextField.getValue()).intValue(); + if (port < 1 || port > 65535) { + throw new Exception(); + } + } catch (Exception x) { + throw new SettingsException("Please specify a valid https port number.", x); + } + return port; + } + + public void saveSettings(int memoryLimit, int port, int httpsPort, String contextPath) throws SettingsException { + File file = getOptionsFile(); + + java.util.List<String> lines = readLines(file); + java.util.List<String> newLines = new ArrayList<String>(); + + boolean memoryLimitAdded = false; + boolean portAdded = false; + boolean httpsPortAdded = false; + boolean contextPathAdded = false; + + for (String line : lines) { + if (line.startsWith("-Xmx")) { + newLines.add("-Xmx" + memoryLimit + "m"); + memoryLimitAdded = true; + } else if (line.startsWith("-Dsubsonic.port=")) { + newLines.add("-Dsubsonic.port=" + port); + portAdded = true; + } else if (line.startsWith("-Dsubsonic.httpsPort=")) { + newLines.add("-Dsubsonic.httpsPort=" + httpsPort); + httpsPortAdded = true; + } else if (line.startsWith("-Dsubsonic.contextPath=")) { + newLines.add("-Dsubsonic.contextPath=" + contextPath); + contextPathAdded = true; + } else { + newLines.add(line); + } + } + + if (!memoryLimitAdded) { + newLines.add("-Xmx" + memoryLimit + "m"); + } + if (!portAdded) { + newLines.add("-Dsubsonic.port=" + port); + } + if (!httpsPortAdded) { + newLines.add("-Dsubsonic.httpsPort=" + httpsPort); + } + if (!contextPathAdded) { + newLines.add("-Dsubsonic.contextPath=" + contextPath); + } + + writeLines(file, newLines); + + JOptionPane.showMessageDialog(SettingsPanel.this, + "Please restart Subsonic for the new settings to take effect.", + "Settings changed", JOptionPane.INFORMATION_MESSAGE); + + } + + private File getOptionsFile() throws SettingsException { + File file = new File("subsonic-service.exe.vmoptions"); + if (!file.isFile() || !file.exists()) { + throw new SettingsException("File " + file.getAbsolutePath() + " not found."); + } + return file; + } + + private List<String> readLines(File file) throws SettingsException { + List<String> lines = new ArrayList<String>(); + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(file)); + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + lines.add(line); + } + return lines; + } catch (IOException x) { + throw new SettingsException("Failed to read from file " + file.getAbsolutePath(), x); + } finally { + closeQuietly(reader); + } + } + + private void writeLines(File file, List<String> lines) throws SettingsException { + PrintWriter writer = null; + try { + writer = new PrintWriter(new FileWriter(file)); + for (String line : lines) { + writer.println(line); + } + } catch (IOException x) { + throw new SettingsException("Failed to write to file " + file.getAbsolutePath(), x); + } finally { + closeQuietly(writer); + } + } + + private String grep(String regexp) throws SettingsException { + Pattern pattern = Pattern.compile(regexp); + File file = getOptionsFile(); + for (String line : readLines(file)) { + Matcher matcher = pattern.matcher(line); + if (matcher.matches()) { + return matcher.group(1); + } + } + return null; + } + + private void closeQuietly(Reader reader) { + if (reader == null) { + return; + } + + try { + reader.close(); + } catch (IOException x) { + // Intentionally ignored. + } + } + + private void closeQuietly(Writer writer) { + if (writer == null) { + return; + } + + try { + writer.close(); + } catch (IOException x) { + // Intentionally ignored. + } + } + + public void notifyDeploymentStatus(DeploymentStatus deploymentStatus) { + // Nothing here yet. + } + + public void notifyServiceStatus(String serviceStatus) { + // Nothing here yet. + } + + public static class SettingsException extends Exception { + + public SettingsException(String message, Throwable cause) { + super(message, cause); + } + + public SettingsException(String message) { + this(message, null); + } + + @Override + public String getMessage() { + if (getCause() == null || getCause().getMessage() == null) { + return super.getMessage(); + } + return super.getMessage() + " " + getCause().getMessage(); + } + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/StatusPanel.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/StatusPanel.java new file mode 100644 index 00000000..91625f19 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/StatusPanel.java @@ -0,0 +1,116 @@ +package net.sourceforge.subsonic.booter.agent; + +import com.jgoodies.forms.builder.DefaultFormBuilder; +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.factories.ButtonBarFactory; +import com.jgoodies.forms.layout.FormLayout; +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.DateFormat; +import java.util.Locale; + +/** + * Panel displaying the status of the Subsonic service. + * + * @author Sindre Mehus + */ +public class StatusPanel extends JPanel implements SubsonicListener { + + private static final DateFormat DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.US); + + private final SubsonicAgent subsonicAgent; + + private JTextField statusTextField; + private JTextField startedTextField; + private JTextField memoryTextField; + private JTextArea errorTextField; + private JButton startButton; + private JButton stopButton; + private JButton urlButton; + + public StatusPanel(SubsonicAgent subsonicAgent) { + this.subsonicAgent = subsonicAgent; + createComponents(); + configureComponents(); + layoutComponents(); + addBehaviour(); + subsonicAgent.addListener(this); + } + + private void createComponents() { + statusTextField = new JTextField(); + startedTextField = new JTextField(); + memoryTextField = new JTextField(); + errorTextField = new JTextArea(3, 24); + startButton = new JButton("Start"); + stopButton = new JButton("Stop"); + urlButton = new JButton(); + } + + private void configureComponents() { + statusTextField.setEditable(false); + startedTextField.setEditable(false); + memoryTextField.setEditable(false); + errorTextField.setEditable(false); + + errorTextField.setLineWrap(true); + errorTextField.setBorder(startedTextField.getBorder()); + + urlButton.setBorderPainted(false); + urlButton.setContentAreaFilled(false); + urlButton.setForeground(Color.BLUE.darker()); + urlButton.setHorizontalAlignment(SwingConstants.LEFT); + } + + private void layoutComponents() { + JPanel buttons = ButtonBarFactory.buildRightAlignedBar(startButton, stopButton); + + FormLayout layout = new FormLayout("right:d, 6dlu, max(d;30dlu):grow"); + DefaultFormBuilder builder = new DefaultFormBuilder(layout, this); + builder.append("Service status", statusTextField); + builder.append("", buttons); + builder.appendParagraphGapRow(); + builder.nextRow(); + builder.append("Started on", startedTextField); + builder.append("Memory used", memoryTextField); + builder.append("Error message", errorTextField); + builder.append("Server address", urlButton); + + setBorder(Borders.DIALOG_BORDER); + } + + private void addBehaviour() { + urlButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + subsonicAgent.openBrowser(); + } + }); + startButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + subsonicAgent.checkElevation("-start"); + subsonicAgent.startOrStopService(true); + } + }); + stopButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + subsonicAgent.checkElevation("-stop"); + subsonicAgent.startOrStopService(false); + } + }); + } + + public void notifyDeploymentStatus(DeploymentStatus status) { + startedTextField.setText(status == null ? null : DATE_FORMAT.format(status.getStartTime())); + memoryTextField.setText(status == null ? null : status.getMemoryUsed() + " MB"); + errorTextField.setText(status == null ? null : status.getErrorMessage()); + urlButton.setText(status == null ? null : status.getURL()); + } + + public void notifyServiceStatus(String serviceStatus) { + statusTextField.setText(serviceStatus); + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicAgent.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicAgent.java new file mode 100644 index 00000000..a9bb526e --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicAgent.java @@ -0,0 +1,201 @@ +package net.sourceforge.subsonic.booter.agent; + +import java.awt.Desktop; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.swing.JOptionPane; +import javax.swing.UIManager; + +import org.apache.commons.io.IOUtils; + +import com.jgoodies.looks.plastic.PlasticXPLookAndFeel; + +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService; + +/** + * Responsible for deploying the Subsonic web app in + * the embedded Jetty container. + * + * @author Sindre Mehus + */ +public class SubsonicAgent { + + private final List<SubsonicListener> listeners = new ArrayList<SubsonicListener>(); + private final TrayController trayController; + private SubsonicFrame frame; + private final SubsonicDeployerService service; + private static final int POLL_INTERVAL_DEPLOYMENT_INFO_SECONDS = 5; + private static final int POLL_INTERVAL_SERVICE_STATUS_SECONDS = 5; + private String url; + private boolean serviceStatusPollingEnabled; + private boolean elevated; + + public SubsonicAgent(SubsonicDeployerService service) { + this.service = service; + setLookAndFeel(); + trayController = new TrayController(this); + startPolling(); + } + + public void setFrame(SubsonicFrame frame) { + this.frame = frame; + } + + private void setLookAndFeel() { + // Set look-and-feel. + try { + UIManager.setLookAndFeel(new PlasticXPLookAndFeel()); + } catch (Throwable x) { + System.err.println("Failed to set look-and-feel.\n" + x); + } + } + + private void startPolling() { + ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + Runnable runnable = new Runnable() { + public void run() { + try { + notifyDeploymentInfo(service.getDeploymentInfo()); + } catch (Throwable x) { + notifyDeploymentInfo(null); + } + } + }; + executor.scheduleWithFixedDelay(runnable, 0, POLL_INTERVAL_DEPLOYMENT_INFO_SECONDS, TimeUnit.SECONDS); + + runnable = new Runnable() { + public void run() { + if (serviceStatusPollingEnabled) { + try { + notifyServiceStatus(getServiceStatus()); + } catch (Throwable x) { + notifyServiceStatus(null); + } + } + } + }; + executor.scheduleWithFixedDelay(runnable, 0, POLL_INTERVAL_SERVICE_STATUS_SECONDS, TimeUnit.SECONDS); + } + + private String getServiceStatus() throws Exception { + Process process = Runtime.getRuntime().exec("subsonic-service.exe -status"); + return IOUtils.toString(process.getInputStream()); + } + + public void setServiceStatusPollingEnabled(boolean enabled) { + serviceStatusPollingEnabled = enabled; + } + + public void startOrStopService(boolean start) { + try { + String cmd = "subsonic-service.exe " + (start ? "-start" : "-stop"); + System.err.println("Executing: " + cmd); + + Runtime.getRuntime().exec(cmd); + } catch (Exception x) { + x.printStackTrace(); + } + } + + /** + * If necessary, restart agent with elevated rights. + */ + public void checkElevation(String... args) { + + if (isElevationNeeded() && !isElevated()) { + try { + List<String> command = new ArrayList<String>(); + command.add("cmd"); + command.add("/c"); + command.add("subsonic-agent-elevated.exe"); + command.addAll(Arrays.asList(args)); + + ProcessBuilder builder = new ProcessBuilder(); + builder.command(command); + System.err.println("Executing: " + command + " with current dir: " + System.getProperty("user.dir")); + builder.start(); + System.exit(0); + } catch (Exception x) { + JOptionPane.showMessageDialog(frame, "Failed to elevate Subsonic Control Panel. " + x, "Error", JOptionPane.WARNING_MESSAGE); + x.printStackTrace(); + } + } + } + + public void setElevated(boolean elevated) { + this.elevated = elevated; + } + + private boolean isElevated() { + return elevated; + } + + /** + * Returns whether UAC elevation is necessary (to start/stop services etc). + */ + private boolean isElevationNeeded() { + + String osVersion = System.getProperty("os.version"); + try { + int majorVersion = Integer.parseInt(osVersion.substring(0, osVersion.indexOf("."))); + + // Elevation is necessary in Windows Vista (os.version=6.1) and later. + return majorVersion >= 6; + } catch (Exception x) { + System.err.println("Failed to resolve OS version from '" + osVersion + "'\n" + x); + return false; + } + } + + public void addListener(SubsonicListener listener) { + listeners.add(listener); + } + + private void notifyDeploymentInfo(DeploymentStatus status) { + if (status != null) { + url = status.getURL(); + } + + for (SubsonicListener listener : listeners) { + listener.notifyDeploymentStatus(status); + } + } + + private void notifyServiceStatus(String status) { + for (SubsonicListener listener : listeners) { + listener.notifyServiceStatus(status); + } + } + + public void showStatusPanel() { + frame.showStatusPanel(); + } + + public void showSettingsPanel() { + frame.showSettingsPanel(); + } + + public void showTrayIconMessage() { + trayController.showMessage(); + } + + public void exit() { + trayController.uninstallComponents(); + System.exit(0); + } + + public void openBrowser() { + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (Throwable x) { + x.printStackTrace(); + } + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicFrame.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicFrame.java new file mode 100644 index 00000000..32ee5230 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicFrame.java @@ -0,0 +1,113 @@ +package net.sourceforge.subsonic.booter.agent; + +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.factories.ButtonBarFactory; +import net.sourceforge.subsonic.booter.Main; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * Frame that is activated by the tray icon. Contains a tabbed pane + * with status and settings panels. + * + * @author Sindre Mehus + */ +public class SubsonicFrame extends JFrame { + + private final SubsonicAgent subsonicAgent; + + private final StatusPanel statusPanel; + private final SettingsPanel settingsPanel; + private JTabbedPane tabbedPane; + private JButton closeButton; + + public SubsonicFrame(SubsonicAgent subsonicAgent, StatusPanel statusPanel, SettingsPanel settingsPanel) { + super("Subsonic Control Panel"); + this.subsonicAgent = subsonicAgent; + this.statusPanel = statusPanel; + this.settingsPanel = settingsPanel; + createComponents(); + layoutComponents(); + addBehaviour(); + setupIcons(); + + pack(); + centerComponent(); + } + + private void setupIcons() { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + + // Window.setIconImages() was added in Java 1.6. Since Subsonic only requires 1.5, we + // use reflection to invoke it. + try { + Method method = Window.class.getMethod("setIconImages", java.util.List.class); + java.util.List<Image> images = Arrays.asList( + toolkit.createImage(Main.class.getResource("/images/subsonic-16.png")), + toolkit.createImage(Main.class.getResource("/images/subsonic-32.png")), + toolkit.createImage(Main.class.getResource("/images/subsonic-512.png"))); + method.invoke(this, images); + } catch (Throwable x) { + // Fallback to old method. + setIconImage(toolkit.createImage(Main.class.getResource("/images/subsonic-32.png"))); + } + } + + public void centerComponent() { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation(screenSize.width / 2 - getWidth() / 2, + screenSize.height / 2 - getHeight() / 2); + } + + private void createComponents() { + tabbedPane = new JTabbedPane(); + closeButton = new JButton("Close"); + } + + private void layoutComponents() { + tabbedPane.add("Status", statusPanel); + tabbedPane.add("Settings", settingsPanel); + + JPanel pane = (JPanel) getContentPane(); + pane.setLayout(new BorderLayout(10, 10)); + pane.add(tabbedPane, BorderLayout.CENTER); + pane.add(ButtonBarFactory.buildCloseBar(closeButton), BorderLayout.SOUTH); + + pane.setBorder(Borders.TABBED_DIALOG_BORDER); + } + + private void addBehaviour() { + closeButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setVisible(false); + } + }); + } + + @Override + public void setVisible(boolean b) { + super.setVisible(b); + subsonicAgent.setServiceStatusPollingEnabled(b); + } + + public void showStatusPanel() { + settingsPanel.readValues(); + tabbedPane.setSelectedComponent(statusPanel); + pack(); + setVisible(true); + toFront(); + } + + public void showSettingsPanel() { + settingsPanel.readValues(); + tabbedPane.setSelectedComponent(settingsPanel); + pack(); + setVisible(true); + toFront(); + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicListener.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicListener.java new file mode 100644 index 00000000..d6239c0d --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicListener.java @@ -0,0 +1,28 @@ +package net.sourceforge.subsonic.booter.agent; + +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; + +/** + * Callback interface implemented by GUI classes that wants to be notified when + * the state of the Subsonic deployment changes. + * + * @author Sindre Mehus + */ +public interface SubsonicListener { + + /** + * Invoked when new information about the Subsonic deployment is available. + * + * @param deploymentStatus The new deployment status, or <code>null</code> if an + * error occurred while retrieving the status. + */ + void notifyDeploymentStatus(DeploymentStatus deploymentStatus); + + /** + * Invoked when new information about the Subsonic Windows service is available. + * + * @param serviceStatus The new service status, or <code>null</code> if an + * error occurred while retrieving the status. + */ + void notifyServiceStatus(String serviceStatus); +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/TrayController.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/TrayController.java new file mode 100644 index 00000000..2be918e8 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/TrayController.java @@ -0,0 +1,125 @@ +package net.sourceforge.subsonic.booter.agent; + +import java.awt.Image; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.event.ActionEvent; +import java.net.URL; + +import javax.swing.AbstractAction; +import javax.swing.Action; + +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; + +/** + * Controls the Subsonic tray icon. + * + * @author Sindre Mehus + */ +public class TrayController implements SubsonicListener { + + private final SubsonicAgent subsonicAgent; + private TrayIcon trayIcon; + + private Action openAction; + private Action controlPanelAction; + private Action hideAction; + private Image startedImage; + private Image stoppedImage; + + public TrayController(SubsonicAgent subsonicAgent) { + this.subsonicAgent = subsonicAgent; + try { + createActions(); + createComponents(); + addBehaviour(); + installComponents(); + subsonicAgent.addListener(this); + } catch (Throwable x) { + System.err.println("Disabling tray support."); + } + } + + public void showMessage() { + trayIcon.displayMessage("Subsonic", "Subsonic is now running. Click this balloon to get started.", + TrayIcon.MessageType.INFO); + } + + private void createActions() { + openAction = new AbstractAction("Open Subsonic in Browser") { + public void actionPerformed(ActionEvent e) { + subsonicAgent.openBrowser(); + } + }; + + controlPanelAction = new AbstractAction("Subsonic Control Panel") { + public void actionPerformed(ActionEvent e) { + subsonicAgent.showStatusPanel(); + } + }; + + + hideAction = new AbstractAction("Hide Tray Icon") { + public void actionPerformed(ActionEvent e) { + subsonicAgent.exit(); + } + }; + } + + private void createComponents() { + startedImage = createImage("/images/subsonic-started-16.png"); + stoppedImage = createImage("/images/subsonic-stopped-16.png"); + + PopupMenu menu = new PopupMenu(); + menu.add(createMenuItem(openAction)); + menu.add(createMenuItem(controlPanelAction)); + menu.addSeparator(); + menu.add(createMenuItem(hideAction)); + + trayIcon = new TrayIcon(stoppedImage, "Subsonic Music Streamer", menu); + } + + private Image createImage(String resourceName) { + URL url = getClass().getResource(resourceName); + return Toolkit.getDefaultToolkit().createImage(url); + } + + private MenuItem createMenuItem(Action action) { + MenuItem menuItem = new MenuItem((String) action.getValue(Action.NAME)); + menuItem.addActionListener(action); + return menuItem; + } + + private void addBehaviour() { + trayIcon.addActionListener(controlPanelAction); + } + + private void installComponents() throws Throwable { + SystemTray.getSystemTray().add(trayIcon); + } + + public void uninstallComponents() { + try { + SystemTray.getSystemTray().remove(trayIcon); + } catch (Throwable x) { + System.err.println("Disabling tray support."); + } + } + + private void setTrayImage(Image image) { + if (trayIcon.getImage() != image) { + trayIcon.setImage(image); + } + } + + public void notifyDeploymentStatus(DeploymentStatus deploymentStatus) { + setTrayImage(deploymentStatus == null ? stoppedImage : startedImage); + } + + public void notifyServiceStatus(String serviceStatus) { + // Nothing here, but could potentially change tray icon and menu. + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/DeploymentStatus.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/DeploymentStatus.java new file mode 100644 index 00000000..3b237dc1 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/DeploymentStatus.java @@ -0,0 +1,44 @@ +package net.sourceforge.subsonic.booter.deployer; + +import java.util.Date; +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class DeploymentStatus implements Serializable { + + private final Date startTime; + private final String url; + private final String httpsUrl; + private final int memoryUsed; + private final String errorMessage; + + public DeploymentStatus(Date startTime, String url, String httpsUrl, int memoryUsed, String errorMessage) { + this.startTime = startTime; + this.url = url; + this.httpsUrl = httpsUrl; + this.memoryUsed = memoryUsed; + this.errorMessage = errorMessage; + } + + public String getURL() { + return url; + } + + public String getHttpsUrl() { + return httpsUrl; + } + + public Date getStartTime() { + return startTime; + } + + public int getMemoryUsed() { + return memoryUsed; + } + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployer.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployer.java new file mode 100644 index 00000000..b066c635 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployer.java @@ -0,0 +1,326 @@ +package net.sourceforge.subsonic.booter.deployer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.net.BindException; +import java.util.Date; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +import org.apache.commons.io.IOUtils; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.security.Constraint; +import org.mortbay.jetty.security.ConstraintMapping; +import org.mortbay.jetty.security.SslSocketConnector; +import org.mortbay.jetty.nio.SelectChannelConnector; +import org.mortbay.jetty.webapp.WebAppContext; + +/** + * Responsible for deploying the Subsonic web app in + * the embedded Jetty container. + * <p/> + * The following system properties may be used to customize the behaviour: + * <ul> + * <li><code>subsonic.contextPath</code> - The context path at which Subsonic is deployed. Default "/".</li> + * <li><code>subsonic.port</code> - The port Subsonic will listen to. Default 4040.</li> + * <li><code>subsonic.httpsPort</code> - The port Subsonic will listen to for HTTPS. Default 0, which disables HTTPS.</li> + * <li><code>subsonic.war</code> - Subsonic WAR file, or exploded directory. Default "subsonic.war".</li> + * <li><code>subsonic.createLinkFile</code> - If set to "true", a Subsonic.url file is created in the working directory.</li> + * <li><code>subsonic.ssl.keystore</code> - Path to an alternate SSL keystore.</li> + * <li><code>subsonic.ssl.password</code> - Password of the alternate SSL keystore.</li> + * </ul> + * + * @author Sindre Mehus + */ +public class SubsonicDeployer implements SubsonicDeployerService { + + public static final String DEFAULT_HOST = "0.0.0.0"; + public static final int DEFAULT_PORT = 4040; + public static final int DEFAULT_HTTPS_PORT = 0; + public static final int DEFAULT_MEMORY_LIMIT = 150; + public static final String DEFAULT_CONTEXT_PATH = "/"; + public static final String DEFAULT_WAR = "subsonic.war"; + private static final int MAX_IDLE_TIME_MILLIS = 7 * 24 * 60 * 60 * 1000; // One week. + private static final int HEADER_BUFFER_SIZE = 64 * 1024; + + // Subsonic home directory. + private static final File SUBSONIC_HOME_WINDOWS = new File("c:/subsonic"); + private static final File SUBSONIC_HOME_OTHER = new File("/var/subsonic"); + + private Throwable exception; + private File subsonicHome; + private final Date startTime; + + public SubsonicDeployer() { + + // Enable shutdown hook for Ehcache. + System.setProperty("net.sf.ehcache.enableShutdownHook", "true"); + + startTime = new Date(); + createLinkFile(); + deployWebApp(); + } + + private void createLinkFile() { + if ("true".equals(System.getProperty("subsonic.createLinkFile"))) { + Writer writer = null; + try { + writer = new FileWriter("subsonic.url"); + writer.append("[InternetShortcut]"); + writer.append(System.getProperty("line.separator")); + writer.append("URL=").append(getUrl()); + writer.flush(); + } catch (Throwable x) { + System.err.println("Failed to create subsonic.url."); + x.printStackTrace(); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException x) { + // Ignored + } + } + } + } + } + + private void deployWebApp() { + try { + Server server = new Server(); + SelectChannelConnector connector = new SelectChannelConnector(); + connector.setMaxIdleTime(MAX_IDLE_TIME_MILLIS); + connector.setHeaderBufferSize(HEADER_BUFFER_SIZE); + connector.setHost(getHost()); + connector.setPort(getPort()); + if (isHttpsEnabled()) { + connector.setConfidentialPort(getHttpsPort()); + } + server.addConnector(connector); + + if (isHttpsEnabled()) { + SslSocketConnector sslConnector = new SslSocketConnector(); + sslConnector.setMaxIdleTime(MAX_IDLE_TIME_MILLIS); + sslConnector.setHeaderBufferSize(HEADER_BUFFER_SIZE); + sslConnector.setHost(getHost()); + sslConnector.setPort(getHttpsPort()); + sslConnector.setKeystore(System.getProperty("subsonic.ssl.keystore", getClass().getResource("/subsonic.keystore").toExternalForm())); + sslConnector.setPassword(System.getProperty("subsonic.ssl.password", "subsonic")); + server.addConnector(sslConnector); + } + + WebAppContext context = new WebAppContext(); + context.setTempDirectory(getJettyDirectory()); + context.setContextPath(getContextPath()); + context.setWar(getWar()); + + if (isHttpsEnabled()) { + ConstraintMapping constraintMapping = new ConstraintMapping(); + Constraint constraint = new Constraint(); + constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL); + constraintMapping.setPathSpec("/"); + constraintMapping.setConstraint(constraint); + context.getSecurityHandler().setConstraintMappings(new ConstraintMapping[]{constraintMapping}); + } + + server.addHandler(context); + server.start(); + + System.err.println("Subsonic running on: " + getUrl()); + if (isHttpsEnabled()) { + System.err.println(" and: " + getHttpsUrl()); + } + + } catch (Throwable x) { + x.printStackTrace(); + exception = x; + } + } + + private File getJettyDirectory() { + File dir = new File(getSubsonicHome(), "jetty"); + String buildNumber = getSubsonicBuildNumber(); + if (buildNumber != null) { + dir = new File(dir, buildNumber); + } + System.err.println("Extracting webapp to " + dir); + + if (!dir.exists() && !dir.mkdirs()) { + System.err.println("Failed to create directory " + dir); + } + + return dir; + } + + private String getSubsonicBuildNumber() { + File war = new File(getWar()); + InputStream in = null; + try { + if (war.isFile()) { + JarFile jar = new JarFile(war); + ZipEntry entry = jar.getEntry("WEB-INF\\classes\\build_number.txt"); + if (entry == null) { + entry = jar.getEntry("WEB-INF/classes/build_number.txt"); + } + in = jar.getInputStream(entry); + } else { + in = new FileInputStream(war.getPath() + "/WEB-INF/classes/build_number.txt"); + } + return IOUtils.toString(in); + + } catch (Exception x) { + System.err.println("Failed to resolve build number from WAR: " + war); + x.printStackTrace(); + return null; + } finally { + IOUtils.closeQuietly(in); + } + } + + private String getContextPath() { + return System.getProperty("subsonic.contextPath", DEFAULT_CONTEXT_PATH); + } + + + private String getWar() { + String war = System.getProperty("subsonic.war"); + if (war == null) { + war = DEFAULT_WAR; + } + + File file = new File(war); + if (file.exists()) { + System.err.println("Using WAR file: " + file.getAbsolutePath()); + } else { + System.err.println("Error: WAR file not found: " + file.getAbsolutePath()); + } + + return war; + } + + private String getHost() { + return System.getProperty("subsonic.host", DEFAULT_HOST); + } + + private int getPort() { + int port = DEFAULT_PORT; + + String portString = System.getProperty("subsonic.port"); + if (portString != null) { + port = Integer.parseInt(portString); + } + + // Also set it so that the webapp can read it. + System.setProperty("subsonic.port", String.valueOf(port)); + + return port; + } + + private int getHttpsPort() { + int port = DEFAULT_HTTPS_PORT; + + String portString = System.getProperty("subsonic.httpsPort"); + if (portString != null) { + port = Integer.parseInt(portString); + } + + // Also set it so that the webapp can read it. + System.setProperty("subsonic.httpsPort", String.valueOf(port)); + + return port; + } + + private boolean isHttpsEnabled() { + return getHttpsPort() > 0; + } + + public String getErrorMessage() { + if (exception == null) { + return null; + } + if (exception instanceof BindException) { + return "Address already in use. Please change port number."; + } + + return exception.toString(); + } + + public int getMemoryUsed() { + long freeBytes = Runtime.getRuntime().freeMemory(); + long totalBytes = Runtime.getRuntime().totalMemory(); + long usedBytes = totalBytes - freeBytes; + return (int) Math.round(usedBytes / 1024.0 / 1024.0); + } + + private String getUrl() { + String host = DEFAULT_HOST.equals(getHost()) ? "localhost" : getHost(); + StringBuffer url = new StringBuffer("http://").append(host); + if (getPort() != 80) { + url.append(":").append(getPort()); + } + url.append(getContextPath()); + return url.toString(); + } + + private String getHttpsUrl() { + if (!isHttpsEnabled()) { + return null; + } + + String host = DEFAULT_HOST.equals(getHost()) ? "localhost" : getHost(); + StringBuffer url = new StringBuffer("https://").append(host); + if (getHttpsPort() != 443) { + url.append(":").append(getHttpsPort()); + } + url.append(getContextPath()); + return url.toString(); + } + + /** + * Returns the Subsonic home directory. + * + * @return The Subsonic home directory, if it exists. + * @throws RuntimeException If directory doesn't exist. + */ + private File getSubsonicHome() { + + if (subsonicHome != null) { + return subsonicHome; + } + + File home; + + String overrideHome = System.getProperty("subsonic.home"); + if (overrideHome != null) { + home = new File(overrideHome); + } else { + boolean isWindows = System.getProperty("os.name", "Windows").toLowerCase().startsWith("windows"); + home = isWindows ? SUBSONIC_HOME_WINDOWS : SUBSONIC_HOME_OTHER; + } + + // Attempt to create home directory if it doesn't exist. + if (!home.exists() || !home.isDirectory()) { + boolean success = home.mkdirs(); + if (success) { + subsonicHome = home; + } else { + String message = "The directory " + home + " does not exist. Please create it and make it writable. " + + "(You can override the directory location by specifying -Dsubsonic.home=... when " + + "starting the servlet container.)"; + System.err.println("ERROR: " + message); + } + } else { + subsonicHome = home; + } + + return home; + } + + public DeploymentStatus getDeploymentInfo() { + return new DeploymentStatus(startTime, getUrl(), getHttpsUrl(), getMemoryUsed(), getErrorMessage()); + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployerService.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployerService.java new file mode 100644 index 00000000..a0bf087f --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployerService.java @@ -0,0 +1,17 @@ +package net.sourceforge.subsonic.booter.deployer; + +/** + * RMI interface implemented by the Subsonic deployer and used by the agent. + * + * @author Sindre Mehus + */ +public interface SubsonicDeployerService { + + /** + * Returns information about the Subsonic deployment, such + * as URL, memory consumption, start time etc. + * + * @return Deployment information. + */ + DeploymentStatus getDeploymentInfo(); +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/StatusPanel.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/StatusPanel.java new file mode 100644 index 00000000..f20671f8 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/StatusPanel.java @@ -0,0 +1,115 @@ +package net.sourceforge.subsonic.booter.mac; + +import java.awt.Color; +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.net.URI; +import java.text.DateFormat; +import java.util.Locale; + +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingConstants; +import javax.swing.Timer; + +import com.jgoodies.forms.builder.DefaultFormBuilder; +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.layout.FormLayout; + +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService; + +/** + * Panel displaying the status of the Subsonic service. + * + * @author Sindre Mehus + */ +public class StatusPanel extends JPanel { + + private static final DateFormat DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.US); + + private final SubsonicDeployerService deployer; + + private JTextField startedTextField; + private JTextField memoryTextField; + private JTextArea errorTextField; + private JButton urlButton; + + public StatusPanel(SubsonicDeployerService deployer) { + this.deployer = deployer; + createComponents(); + configureComponents(); + layoutComponents(); + addBehaviour(); + } + + private void createComponents() { + startedTextField = new JTextField(); + memoryTextField = new JTextField(); + errorTextField = new JTextArea(3, 24); + urlButton = new JButton(); + } + + private void configureComponents() { + startedTextField.setEditable(false); + memoryTextField.setEditable(false); + errorTextField.setEditable(false); + + errorTextField.setLineWrap(true); + errorTextField.setBorder(startedTextField.getBorder()); + + urlButton.setBorderPainted(false); + urlButton.setContentAreaFilled(false); + urlButton.setForeground(Color.BLUE.darker()); + urlButton.setHorizontalAlignment(SwingConstants.LEFT); + } + + private void layoutComponents() { + FormLayout layout = new FormLayout("right:d, 6dlu, max(d;30dlu):grow"); + DefaultFormBuilder builder = new DefaultFormBuilder(layout, this); + builder.append("Started on", startedTextField); + builder.append("Memory used", memoryTextField); + builder.append("Error message", errorTextField); + builder.append("Server address", urlButton); + + setBorder(Borders.DIALOG_BORDER); + } + + private void addBehaviour() { + urlButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + openBrowser(); + } + }); + + Timer timer = new Timer(3000, new ActionListener() { + public void actionPerformed(ActionEvent e) { + updateStatus(deployer.getDeploymentInfo()); + } + }); + updateStatus(deployer.getDeploymentInfo()); + timer.start(); + } + + private void openBrowser() { + String url = urlButton.getText(); + if (url == null) { + return; + } + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (Throwable x) { + x.printStackTrace(); + } + } + + private void updateStatus(DeploymentStatus status) { + startedTextField.setText(status == null ? null : DATE_FORMAT.format(status.getStartTime())); + memoryTextField.setText(status == null ? null : status.getMemoryUsed() + " MB"); + errorTextField.setText(status == null ? null : status.getErrorMessage()); + urlButton.setText(status == null ? null : status.getURL()); + } +}
\ No newline at end of file diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicController.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicController.java new file mode 100644 index 00000000..65731f31 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicController.java @@ -0,0 +1,89 @@ +package net.sourceforge.subsonic.booter.mac; + +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.net.URL; +import java.net.URI; + +/** + * Controller for the Mac booter. + * + * @author Sindre Mehus + */ +public class SubsonicController { + + private final SubsonicDeployerService deployer; + private final SubsonicFrame frame; + private Action openAction; + private Action controlPanelAction; + private Action quitAction; + + public SubsonicController(SubsonicDeployerService deployer, SubsonicFrame frame) { + this.deployer = deployer; + this.frame = frame; + createActions(); + createComponents(); + } + + private void createActions() { + openAction = new AbstractAction("Open Subsonic Web Page") { + public void actionPerformed(ActionEvent e) { + openBrowser(); + } + }; + + controlPanelAction = new AbstractAction("Subsonic Control Panel") { + public void actionPerformed(ActionEvent e) { + frame.setActive(false); + frame.setActive(true); + } + }; + + quitAction = new AbstractAction("Quit Subsonic") { + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }; + } + + private void createComponents() { + PopupMenu menu = new PopupMenu(); + menu.add(createMenuItem(openAction)); + menu.add(createMenuItem(controlPanelAction)); + menu.addSeparator(); + menu.add(createMenuItem(quitAction)); + + URL url = getClass().getResource("/images/subsonic-21.png"); + Image image = Toolkit.getDefaultToolkit().createImage(url); + TrayIcon trayIcon = new TrayIcon(image, "Subsonic Music Streamer", menu); + trayIcon.setImageAutoSize(false); + + try { + SystemTray.getSystemTray().add(trayIcon); + } catch (Throwable x) { + System.err.println("Failed to add tray icon."); + } + } + + private MenuItem createMenuItem(Action action) { + MenuItem menuItem = new MenuItem((String) action.getValue(Action.NAME)); + menuItem.addActionListener(action); + return menuItem; + } + + private void openBrowser() { + String url = deployer.getDeploymentInfo().getURL(); + if (url == null) { + return; + } + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (Throwable x) { + x.printStackTrace(); + } + } + +}
\ No newline at end of file diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicFrame.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicFrame.java new file mode 100644 index 00000000..2a492e45 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicFrame.java @@ -0,0 +1,82 @@ +package net.sourceforge.subsonic.booter.mac; + +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.factories.ButtonBarFactory; +import net.sourceforge.subsonic.booter.Main; +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.net.URL; + +/** + * Frame with Subsonic status. Used on Mac installs. + * + * @author Sindre Mehus + */ +public class SubsonicFrame extends JFrame { + + private final SubsonicDeployerService deployer; + private StatusPanel statusPanel; + private JButton hideButton; + private JButton exitButton; + + public SubsonicFrame(SubsonicDeployerService deployer) { + super("Subsonic"); + this.deployer = deployer; + createComponents(); + layoutComponents(); + addBehaviour(); + + URL url = Main.class.getResource("/images/subsonic-512.png"); + setIconImage(Toolkit.getDefaultToolkit().createImage(url)); + } + + public void setActive(boolean active) { + if (active) { + pack(); + centerComponent(); + setVisible(true); + toFront(); + } else { + dispose(); + } + } + + private void centerComponent() { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation(screenSize.width / 2 - getWidth() / 2, + screenSize.height / 2 - getHeight() / 2); + } + + private void createComponents() { + statusPanel = new StatusPanel(deployer); + hideButton = new JButton("Hide"); + exitButton = new JButton("Exit"); + } + + private void layoutComponents() { + JPanel pane = (JPanel) getContentPane(); + pane.setLayout(new BorderLayout(10, 10)); + pane.add(statusPanel, BorderLayout.CENTER); + pane.add(ButtonBarFactory.buildRightAlignedBar(hideButton, exitButton), BorderLayout.SOUTH); + + pane.setBorder(Borders.DIALOG_BORDER); + } + + private void addBehaviour() { + hideButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setActive(false); + } + }); + exitButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + } + +}
\ No newline at end of file diff --git a/subsonic-booter/src/main/resources/META-INF/MANIFEST.MF b/subsonic-booter/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 00000000..708d0ddd --- /dev/null +++ b/subsonic-booter/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0
+Main-Class: net.sourceforge.subsonic.booter.Main
diff --git a/subsonic-booter/src/main/resources/applicationContext-agent.xml b/subsonic-booter/src/main/resources/applicationContext-agent.xml new file mode 100644 index 00000000..f54bf9b2 --- /dev/null +++ b/subsonic-booter/src/main/resources/applicationContext-agent.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="service" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
+ <property name="serviceUrl" value="rmi://localhost:9412/SubsonicDeployerService"/>
+ <property name="serviceInterface" value="net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService"/>
+ <property name="lookupStubOnStartup" value="false"/>
+ <property name="refreshStubOnConnectFailure" value="true"/>
+ </bean>
+
+ <bean id="agent" class="net.sourceforge.subsonic.booter.agent.SubsonicAgent">
+ <constructor-arg ref="service"/>
+ <property name="frame" ref="frame"/>
+ </bean>
+
+ <bean id="frame" class="net.sourceforge.subsonic.booter.agent.SubsonicFrame">
+ <constructor-arg ref="agent"/>
+ <constructor-arg ref="statusPanel"/>
+ <constructor-arg ref="settingsPanel"/>
+ </bean>
+
+ <bean id="settingsPanel" class="net.sourceforge.subsonic.booter.agent.SettingsPanel">
+ <constructor-arg ref="agent"/>
+ </bean>
+
+ <bean id="statusPanel" class="net.sourceforge.subsonic.booter.agent.StatusPanel">
+ <constructor-arg ref="agent"/>
+ </bean>
+
+</beans>
\ No newline at end of file diff --git a/subsonic-booter/src/main/resources/applicationContext-deployer.xml b/subsonic-booter/src/main/resources/applicationContext-deployer.xml new file mode 100644 index 00000000..ac4e46de --- /dev/null +++ b/subsonic-booter/src/main/resources/applicationContext-deployer.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="service" class="net.sourceforge.subsonic.booter.deployer.SubsonicDeployer"/>
+
+ <bean class="org.springframework.remoting.rmi.RmiServiceExporter">
+ <property name="serviceName" value="SubsonicDeployerService"/>
+ <property name="service" ref="service"/>
+ <property name="serviceInterface" value="net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService"/>
+ <property name="registryPort" value="9412"/>
+ </bean>
+</beans>
\ No newline at end of file diff --git a/subsonic-booter/src/main/resources/applicationContext-mac.xml b/subsonic-booter/src/main/resources/applicationContext-mac.xml new file mode 100644 index 00000000..36d9a549 --- /dev/null +++ b/subsonic-booter/src/main/resources/applicationContext-mac.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="deployer" class="net.sourceforge.subsonic.booter.deployer.SubsonicDeployer"/>
+
+ <bean id="frame" class="net.sourceforge.subsonic.booter.mac.SubsonicFrame">
+ <constructor-arg ref="deployer"/>
+ </bean>
+
+ <bean id="controller" class="net.sourceforge.subsonic.booter.mac.SubsonicController">
+ <constructor-arg ref="deployer"/>
+ <constructor-arg ref="frame"/>
+ </bean>
+
+</beans>
\ No newline at end of file diff --git a/subsonic-booter/src/main/resources/images/subsonic-16.png b/subsonic-booter/src/main/resources/images/subsonic-16.png Binary files differnew file mode 100644 index 00000000..eba8bb57 --- /dev/null +++ b/subsonic-booter/src/main/resources/images/subsonic-16.png diff --git a/subsonic-booter/src/main/resources/images/subsonic-21.png b/subsonic-booter/src/main/resources/images/subsonic-21.png Binary files differnew file mode 100644 index 00000000..6ce85a4f --- /dev/null +++ b/subsonic-booter/src/main/resources/images/subsonic-21.png diff --git a/subsonic-booter/src/main/resources/images/subsonic-32.png b/subsonic-booter/src/main/resources/images/subsonic-32.png Binary files differnew file mode 100644 index 00000000..b30ed059 --- /dev/null +++ b/subsonic-booter/src/main/resources/images/subsonic-32.png diff --git a/subsonic-booter/src/main/resources/images/subsonic-512.png b/subsonic-booter/src/main/resources/images/subsonic-512.png Binary files differnew file mode 100644 index 00000000..77f183e0 --- /dev/null +++ b/subsonic-booter/src/main/resources/images/subsonic-512.png diff --git a/subsonic-booter/src/main/resources/images/subsonic-started-16.png b/subsonic-booter/src/main/resources/images/subsonic-started-16.png Binary files differnew file mode 100644 index 00000000..8bfcd647 --- /dev/null +++ b/subsonic-booter/src/main/resources/images/subsonic-started-16.png diff --git a/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png b/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png Binary files differnew file mode 100644 index 00000000..c2d637cc --- /dev/null +++ b/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png diff --git a/subsonic-booter/src/main/resources/subsonic.keystore b/subsonic-booter/src/main/resources/subsonic.keystore Binary files differnew file mode 100644 index 00000000..54e1589c --- /dev/null +++ b/subsonic-booter/src/main/resources/subsonic.keystore diff --git a/subsonic-booter/src/main/script/subsonic.bat b/subsonic-booter/src/main/script/subsonic.bat new file mode 100644 index 00000000..2f2b4b13 --- /dev/null +++ b/subsonic-booter/src/main/script/subsonic.bat @@ -0,0 +1,24 @@ +@echo off + +REM The directory where Subsonic will create files. Make sure it is writable. +set SUBSONIC_HOME=c:\subsonic + +REM The host name or IP address on which to bind Subsonic. Only relevant if you have +REM multiple network interfaces and want to make Subsonic available on only one of them. +REM The default value 0.0.0.0 will bind Subsonic to all available network interfaces. +set SUBSONIC_HOST=0.0.0.0 + +REM The port on which Subsonic will listen for incoming HTTP traffic. +set SUBSONIC_PORT=4040 + +REM The port on which Subsonic will listen for incoming HTTPS traffic (0 to disable). +set SUBSONIC_HTTPS_PORT=0 + +REM The context path (i.e., the last part of the Subsonic URL). Typically "/" or "/subsonic". +set SUBSONIC_CONTEXT_PATH=/ + +REM The memory limit (max Java heap size) in megabytes. +set MAX_MEMORY=150 + +java -Xmx%MAX_MEMORY%m -Dsubsonic.home=%SUBSONIC_HOME% -Dsubsonic.host=%SUBSONIC_HOST% -Dsubsonic.port=%SUBSONIC_PORT% -Dsubsonic.httpsPort=%SUBSONIC_HTTPS_PORT% -Dsubsonic.contextPath=%SUBSONIC_CONTEXT_PATH% -jar subsonic-booter-jar-with-dependencies.jar + diff --git a/subsonic-booter/src/main/script/subsonic.sh b/subsonic-booter/src/main/script/subsonic.sh new file mode 100644 index 00000000..4022fb72 --- /dev/null +++ b/subsonic-booter/src/main/script/subsonic.sh @@ -0,0 +1,134 @@ +#!/bin/sh + +################################################################################### +# Shell script for starting Subsonic. See http://subsonic.org. +# +# Author: Sindre Mehus +################################################################################### + +SUBSONIC_HOME=/var/subsonic +SUBSONIC_HOST=0.0.0.0 +SUBSONIC_PORT=4040 +SUBSONIC_HTTPS_PORT=0 +SUBSONIC_CONTEXT_PATH=/ +SUBSONIC_MAX_MEMORY=150 +SUBSONIC_PIDFILE= +SUBSONIC_DEFAULT_MUSIC_FOLDER=/var/music +SUBSONIC_DEFAULT_PODCAST_FOLDER=/var/music/Podcast +SUBSONIC_DEFAULT_PLAYLIST_FOLDER=/var/playlists + +quiet=0 + +usage() { + echo "Usage: subsonic.sh [options]" + echo " --help This small usage guide." + echo " --home=DIR The directory where Subsonic will create files." + echo " Make sure it is writable. Default: /var/subsonic" + echo " --host=HOST The host name or IP address on which to bind Subsonic." + echo " Only relevant if you have multiple network interfaces and want" + echo " to make Subsonic available on only one of them. The default value" + echo " will bind Subsonic to all available network interfaces. Default: 0.0.0.0" + echo " --port=PORT The port on which Subsonic will listen for" + echo " incoming HTTP traffic. Default: 4040" + echo " --https-port=PORT The port on which Subsonic will listen for" + echo " incoming HTTPS traffic. Default: 0 (disabled)" + echo " --context-path=PATH The context path, i.e., the last part of the Subsonic" + echo " URL. Typically '/' or '/subsonic'. Default '/'" + echo " --max-memory=MB The memory limit (max Java heap size) in megabytes." + echo " Default: 100" + echo " --pidfile=PIDFILE Write PID to this file. Default not created." + echo " --quiet Don't print anything to standard out. Default false." + echo " --default-music-folder=DIR Configure Subsonic to use this folder for music. This option " + echo " only has effect the first time Subsonic is started. Default '/var/music'" + echo " --default-podcast-folder=DIR Configure Subsonic to use this folder for Podcasts. This option " + echo " only has effect the first time Subsonic is started. Default '/var/music/Podcast'" + echo " --default-playlist-folder=DIR Configure Subsonic to use this folder for playlists. This option " + echo " only has effect the first time Subsonic is started. Default '/var/playlists'" + exit 1 +} + +# Parse arguments. +while [ $# -ge 1 ]; do + case $1 in + --help) + usage + ;; + --home=?*) + SUBSONIC_HOME=${1#--home=} + ;; + --host=?*) + SUBSONIC_HOST=${1#--host=} + ;; + --port=?*) + SUBSONIC_PORT=${1#--port=} + ;; + --https-port=?*) + SUBSONIC_HTTPS_PORT=${1#--https-port=} + ;; + --context-path=?*) + SUBSONIC_CONTEXT_PATH=${1#--context-path=} + ;; + --max-memory=?*) + SUBSONIC_MAX_MEMORY=${1#--max-memory=} + ;; + --pidfile=?*) + SUBSONIC_PIDFILE=${1#--pidfile=} + ;; + --quiet) + quiet=1 + ;; + --default-music-folder=?*) + SUBSONIC_DEFAULT_MUSIC_FOLDER=${1#--default-music-folder=} + ;; + --default-podcast-folder=?*) + SUBSONIC_DEFAULT_PODCAST_FOLDER=${1#--default-podcast-folder=} + ;; + --default-playlist-folder=?*) + SUBSONIC_DEFAULT_PLAYLIST_FOLDER=${1#--default-playlist-folder=} + ;; + *) + usage + ;; + esac + shift +done + +# Use JAVA_HOME if set, otherwise assume java is in the path. +JAVA=java +if [ -e "${JAVA_HOME}" ] + then + JAVA=${JAVA_HOME}/bin/java +fi + +# Create Subsonic home directory. +mkdir -p ${SUBSONIC_HOME} +LOG=${SUBSONIC_HOME}/subsonic_sh.log +rm -f ${LOG} + +cd $(dirname $0) +if [ -L $0 ] && ([ -e /bin/readlink ] || [ -e /usr/bin/readlink ]); then + cd $(dirname $(readlink $0)) +fi + +${JAVA} -Xmx${SUBSONIC_MAX_MEMORY}m \ + -Dsubsonic.home=${SUBSONIC_HOME} \ + -Dsubsonic.host=${SUBSONIC_HOST} \ + -Dsubsonic.port=${SUBSONIC_PORT} \ + -Dsubsonic.httpsPort=${SUBSONIC_HTTPS_PORT} \ + -Dsubsonic.contextPath=${SUBSONIC_CONTEXT_PATH} \ + -Dsubsonic.defaultMusicFolder=${SUBSONIC_DEFAULT_MUSIC_FOLDER} \ + -Dsubsonic.defaultPodcastFolder=${SUBSONIC_DEFAULT_PODCAST_FOLDER} \ + -Dsubsonic.defaultPlaylistFolder=${SUBSONIC_DEFAULT_PLAYLIST_FOLDER} \ + -Djava.awt.headless=true \ + -verbose:gc \ + -jar subsonic-booter-jar-with-dependencies.jar > ${LOG} 2>&1 & + +# Write pid to pidfile if it is defined. +if [ $SUBSONIC_PIDFILE ]; then + echo $! > ${SUBSONIC_PIDFILE} +fi + +if [ $quiet = 0 ]; then + echo Started Subsonic [PID $!, ${LOG}] +fi + |