aboutsummaryrefslogtreecommitdiff
path: root/subsonic-booter
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2012-07-02 21:24:02 -0700
committerScott Jackson <daneren2005@gmail.com>2012-07-02 21:24:02 -0700
commita1a18f77a50804e0127dfa4b0f5240c49c541184 (patch)
tree19a38880afe505beddb5590379a8134d7730a277 /subsonic-booter
parentb61d787706979e7e20f4c3c4f93c1f129d92273f (diff)
downloaddsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.gz
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.bz2
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.zip
Initial Commit
Diffstat (limited to 'subsonic-booter')
-rw-r--r--subsonic-booter/pom.xml100
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/Main.java65
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SettingsPanel.java375
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/StatusPanel.java116
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicAgent.java201
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicFrame.java113
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicListener.java28
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/TrayController.java125
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/DeploymentStatus.java44
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployer.java326
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployerService.java17
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/StatusPanel.java115
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicController.java89
-rw-r--r--subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicFrame.java82
-rw-r--r--subsonic-booter/src/main/resources/META-INF/MANIFEST.MF2
-rw-r--r--subsonic-booter/src/main/resources/applicationContext-agent.xml33
-rw-r--r--subsonic-booter/src/main/resources/applicationContext-deployer.xml15
-rw-r--r--subsonic-booter/src/main/resources/applicationContext-mac.xml18
-rw-r--r--subsonic-booter/src/main/resources/images/subsonic-16.pngbin0 -> 734 bytes
-rw-r--r--subsonic-booter/src/main/resources/images/subsonic-21.pngbin0 -> 673 bytes
-rw-r--r--subsonic-booter/src/main/resources/images/subsonic-32.pngbin0 -> 1444 bytes
-rw-r--r--subsonic-booter/src/main/resources/images/subsonic-512.pngbin0 -> 33807 bytes
-rw-r--r--subsonic-booter/src/main/resources/images/subsonic-started-16.pngbin0 -> 810 bytes
-rw-r--r--subsonic-booter/src/main/resources/images/subsonic-stopped-16.pngbin0 -> 771 bytes
-rw-r--r--subsonic-booter/src/main/resources/subsonic.keystorebin0 -> 1237 bytes
-rw-r--r--subsonic-booter/src/main/script/subsonic.bat24
-rw-r--r--subsonic-booter/src/main/script/subsonic.sh134
27 files changed, 2022 insertions, 0 deletions
diff --git a/subsonic-booter/pom.xml b/subsonic-booter/pom.xml
new file mode 100644
index 00000000..3078573a
--- /dev/null
+++ b/subsonic-booter/pom.xml
@@ -0,0 +1,100 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>net.sourceforge.subsonic</groupId>
+ <artifactId>subsonic-booter</artifactId>
+ <name>Subsonic Booter</name>
+
+ <parent>
+ <groupId>net.sourceforge.subsonic</groupId>
+ <artifactId>subsonic</artifactId>
+ <version>4.7.beta2</version>
+ </parent>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.mortbay.jetty</groupId>
+ <artifactId>jetty</artifactId>
+ <version>6.1.5</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mortbay.jetty</groupId>
+ <artifactId>jetty-embedded</artifactId>
+ <version>6.1.5</version>
+ <exclusions>
+ <exclusion>
+ <groupId>javax.servlet.jsp</groupId>
+ <artifactId>jsp-api</artifactId>
+ </exclusion>
+ </exclusions>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mortbay.jetty</groupId>
+ <artifactId>jsp-2.0</artifactId>
+ <version>6.1.5</version>
+ <type>pom</type>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.jgoodies</groupId>
+ <artifactId>looks</artifactId>
+ <version>2.1.4</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.jgoodies</groupId>
+ <artifactId>forms</artifactId>
+ <version>1.1.0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring</artifactId>
+ <version>2.5.6</version>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>1.3.1</version>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <finalName>subsonic-booter</finalName>
+ <plugins>
+
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ <archive>
+ <manifestFile>${basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
+ </archive>
+ </configuration>
+
+ <!-- Create executable jar file with all dependencies during 'package' phase. -->
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>attached</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
+ </plugins>
+ </build>
+
+</project>
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
new file mode 100644
index 00000000..eba8bb57
--- /dev/null
+++ b/subsonic-booter/src/main/resources/images/subsonic-16.png
Binary files differ
diff --git a/subsonic-booter/src/main/resources/images/subsonic-21.png b/subsonic-booter/src/main/resources/images/subsonic-21.png
new file mode 100644
index 00000000..6ce85a4f
--- /dev/null
+++ b/subsonic-booter/src/main/resources/images/subsonic-21.png
Binary files differ
diff --git a/subsonic-booter/src/main/resources/images/subsonic-32.png b/subsonic-booter/src/main/resources/images/subsonic-32.png
new file mode 100644
index 00000000..b30ed059
--- /dev/null
+++ b/subsonic-booter/src/main/resources/images/subsonic-32.png
Binary files differ
diff --git a/subsonic-booter/src/main/resources/images/subsonic-512.png b/subsonic-booter/src/main/resources/images/subsonic-512.png
new file mode 100644
index 00000000..77f183e0
--- /dev/null
+++ b/subsonic-booter/src/main/resources/images/subsonic-512.png
Binary files differ
diff --git a/subsonic-booter/src/main/resources/images/subsonic-started-16.png b/subsonic-booter/src/main/resources/images/subsonic-started-16.png
new file mode 100644
index 00000000..8bfcd647
--- /dev/null
+++ b/subsonic-booter/src/main/resources/images/subsonic-started-16.png
Binary files differ
diff --git a/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png b/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png
new file mode 100644
index 00000000..c2d637cc
--- /dev/null
+++ b/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png
Binary files differ
diff --git a/subsonic-booter/src/main/resources/subsonic.keystore b/subsonic-booter/src/main/resources/subsonic.keystore
new file mode 100644
index 00000000..54e1589c
--- /dev/null
+++ b/subsonic-booter/src/main/resources/subsonic.keystore
Binary files differ
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
+