First commit

This commit is contained in:
Sindre Mehus
2016-05-01 17:57:16 +02:00
commit a40ad1f7ef
1340 changed files with 107457 additions and 0 deletions
+100
View File
@@ -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>5.3</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>
@@ -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));
}
}
@@ -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();
}
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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.
}
}
@@ -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;
}
}
@@ -0,0 +1,338 @@
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.nio.SelectChannelConnector;
import org.mortbay.jetty.security.Constraint;
import org.mortbay.jetty.security.ConstraintMapping;
import org.mortbay.jetty.security.SslSocketConnector;
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());
context.setOverrideDescriptor("/web-jetty.xml");
if (isHttpsEnabled()) {
// Allow non-https for streaming and cover art (for Chromecast, UPnP, Sonos etc)
context.getSecurityHandler().setConstraintMappings(new ConstraintMapping[]{
createConstraintMapping("/stream", Constraint.DC_NONE),
createConstraintMapping("/coverArt.view", Constraint.DC_NONE),
createConstraintMapping("/ws/*", Constraint.DC_NONE),
createConstraintMapping("/sonos/*", Constraint.DC_NONE),
createConstraintMapping("/", Constraint.DC_CONFIDENTIAL)
});
}
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 ConstraintMapping createConstraintMapping(String pathSpec, int dataConstraint) {
ConstraintMapping constraintMapping = new ConstraintMapping();
Constraint constraint = new Constraint();
constraint.setDataConstraint(dataConstraint);
constraintMapping.setPathSpec(pathSpec);
constraintMapping.setConstraint(constraint);
return constraintMapping;
}
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);
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();
StringBuilder url = new StringBuilder("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();
StringBuilder url = new StringBuilder("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());
}
}
@@ -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();
}
@@ -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());
}
}
@@ -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();
}
}
}
@@ -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);
}
});
}
}
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Main-Class: net.sourceforge.subsonic.booter.Main
@@ -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>
@@ -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>
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
~ This file is part of Subsonic.
~
~ Subsonic is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ Subsonic is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
~
~ Copyright 2014 (C) Sindre Mehus
-->
<web-app id="subsonic" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!--Enable compression for XML, JSON and JSONP in the REST API-->
<filter>
<filter-name>GzipFilter</filter-name>
<filter-class>org.mortbay.servlet.GzipFilter</filter-class>
<init-param>
<param-name>mimeTypes</param-name>
<param-value>text/xml,application/json,text/javascript</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>GzipFilter</filter-name>
<url-pattern>/rest/*</url-pattern>
</filter-mapping>
</web-app>
@@ -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
+134
View File
@@ -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