/* ==================================================================
* CmdlineSystemService.java - 10/02/2017 3:16:47 PM
*
* Copyright 2007-2017 SolarNetwork.net Dev Team
*
* This program 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 2 of
* the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.system.cmdline;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import net.solarnetwork.node.SystemService;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier;
/**
* SystemService implementation using OS command line actions to perform
* functions.
*
* @author matt
* @version 1.0
*/
public class CmdlineSystemService implements SystemService, SettingSpecifierProvider {
/** The default value for the {@code exitCommand} property. */
public static final String DEFAULT_EXIT_COMMAND = "sudo systemctl restart solarnode";
/** The default value for the {@code rebootCommand} property. */
public static final String DEFAULT_REBOOT_COMMAND = "sudo reboot";
private String exitCommand = DEFAULT_EXIT_COMMAND;
private String rebootCommand = DEFAULT_REBOOT_COMMAND;
private BundleContext bundleContext;
private MessageSource messageSource;
private Thread shutdownThread;
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
public synchronized void exit(final boolean syncState) {
if ( shutdownThread != null ) {
return;
}
log.warn("Restart requested");
final BundleContext ctx = bundleContext;
shutdownThread = new Thread(new Runnable() {
@Override
public void run() {
log.warn("Restart sequence initiated");
// pause slightly at start to give time for original calling thread time to complete
try {
Thread.sleep(1000);
} catch ( Exception e ) {
// ignore
}
final long start = System.currentTimeMillis();
// start another thread to monitor the shutdown process, in case OSGi takes too long or gets hung up
Thread shutdownMonitorThread = new Thread(new Runnable() {
@Override
public void run() {
try {
shutdownThread.join(8000);
final long end = (System.currentTimeMillis() - start);
if ( end < 900 ) {
Thread.sleep(2000);
}
} catch ( Exception e ) {
// ignore
} finally {
if ( syncState ) {
System.err.println("Exiting via command: " + exitCommand);
handleOSCommand(exitCommand);
} else {
System.err.println("Exiting from shutdown request.");
System.exit(0);
}
}
}
}, "System Service Shutdown Monitor");
shutdownMonitorThread.setDaemon(true);
shutdownMonitorThread.start();
if ( ctx != null ) {
try {
log.warn("Stopping OSGi from shutdown request...");
ctx.getBundle(0).stop(org.osgi.framework.Bundle.STOP_TRANSIENT);
} catch ( Exception e ) {
System.err.println("Exception shutting down OSGi: " + e);
}
}
}
}, "System Service Shutdown");
shutdownThread.start();
}
@Override
public void reboot() {
if ( shutdownThread != null ) {
return;
}
log.warn("Reboot requested");
shutdownThread = new Thread(new Runnable() {
@Override
public void run() {
log.warn("Reboot sequence initiated");
// pause slightly at start to give time for original calling thread time to complete
try {
Thread.sleep(1000);
} catch ( Exception e ) {
// ignore
}
handleOSCommand(rebootCommand);
}
}, "System Service Reboot");
shutdownThread.start();
}
private void handleOSCommand(String command) {
if ( command == null ) {
return;
}
ProcessBuilder pb = new ProcessBuilder(command.split("\\s+"));
try {
Process pr = pb.start();
logInputStream(pr.getInputStream(), false);
logInputStream(pr.getErrorStream(), true);
pr.waitFor();
if ( pr.exitValue() == 0 ) {
log.debug("Command [{}] executed", command);
} else {
log.error("Error executing [{}], exit status: {}", command, pr.exitValue());
}
} catch ( IOException e ) {
throw new RuntimeException(e);
} catch ( InterruptedException e ) {
throw new RuntimeException(e);
}
}
private void logInputStream(final InputStream src, final boolean errorStream) {
new Thread(new Runnable() {
@Override
public void run() {
Scanner sc = new Scanner(src);
try {
while ( sc.hasNextLine() ) {
if ( errorStream ) {
log.error(sc.nextLine());
} else {
log.info(sc.nextLine());
}
}
} finally {
sc.close();
}
}
}).start();
}
@Override
public String getSettingUID() {
return "net.solarnetwork.node.system.cmdline.CmdlineSystemService";
}
@Override
public String getDisplayName() {
return "Command Line System Service";
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(2);
CmdlineSystemService defaults = new CmdlineSystemService();
results.add(new BasicTextFieldSettingSpecifier("exitCommand", defaults.exitCommand));
results.add(new BasicTextFieldSettingSpecifier("rebootCommand", defaults.rebootCommand));
return results;
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
/**
* Set a message source to use for i18n messages.
*
* @param messageSource
* The message source to use.
*/
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* Set a bundle context to use.
*
* @param bundleContext
* The bundle context.
*/
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
/**
* Set the OS command to use to exit and sync the application state. The
* command will be split on whitespace to turn into command-line arguments.
*
* @param exitCommand
* The command and arguments to use when {@link #exit(boolean)} is
* called with a {@code true} argument.
*/
public void setExitCommand(String exitCommand) {
this.exitCommand = exitCommand;
}
/**
* Set the OS command to use to reboot the device the application is running
* on. The command will be split on whitespace to turn into command-line
* arguments.
*
* @param exitCommand
* The command and arguments to use when {@link #reboot()} is called.
*/
public void setRebootCommand(String rebootCommand) {
this.rebootCommand = rebootCommand;
}
}