marytts.modules.ExternalModule Maven / Gradle / Ivy
/**
* Copyright 2000-2006 DFKI GmbH.
* All Rights Reserved. Use is subject to license terms.
*
* This file is part of MARY TTS.
*
* MARY TTS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
*/
package marytts.modules;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.LinkedList;
import java.util.Locale;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioSystem;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import marytts.datatypes.MaryData;
import marytts.datatypes.MaryDataType;
import marytts.exceptions.NoSuchPropertyException;
import marytts.modules.synthesis.Voice;
import marytts.server.MaryProperties;
import marytts.util.MaryUtils;
import marytts.util.io.StreamLogger;
import org.apache.log4j.Logger;
import org.xml.sax.SAXException;
/**
* A base class for all external Mary modules. Provides non-specific input/output functionality for communication with an external
* module.
*
* Any external module extending this class will need to implement a constructor calling this class's constructor. If data
* input/output requires additional processing, the subclass may override externalIO()
, open()
and/or
* close()
.
*
* Example for a subclass:
*
* Default case (external module reads from stdin and writes to stdout without requiring any particular triggers):
*
*
* public class Intonation extends ExternalModule {
* public Intonation() {
* super("Intonation", System.getProperty("mary.base") + File.separator + "src" + File.separator + "modules"
* + File.separator + "intonation" + File.separator + "intonation_tts", MaryDataType.INTONISED);
* }
* }
*
*
* Non-standard case (external module needs special trigger, data needs to be post-processed):
*
*
* public class Tokeniser extends ExternalModule {
* public Tokeniser()
* {
* super(...);
* }
*
* protected MaryData externalIO(MaryData d) throws TransformerConfigurationException, TransformerException,
* FileNotFoundException, IOException, ParserConfigurationException, SAXException, Exception {
* MaryData result;
* // Write to and read from external module similarly to super class,
* // but write e.g. an empty line after writing the data,
* // to mark end of input.
*
* // Modify the result tree
* myModifications(result);
*
* return result;
* }
* }
*
*
* @author Marc Schröder
*/
public class ExternalModule implements MaryModule {
private String name;
private String cmd;
private MaryDataType inputType;
private MaryDataType outputType;
private Locale locale;
protected int state;
protected Process process;
protected OutputStream to;
protected InputStream from;
protected StreamLogger errorLogger;
private LinkedList requestQueue;
private boolean exitRequested = false;
protected ProcessingThread processingThread = null;
protected RestarterThread restarterThread = null;
private boolean needToRestart = false;
/**
* The logger instance to be used by this module. It will identify the origin of the log message in the log file.
*/
protected Logger logger;
/**
* The duration given to the module before timeout occurs (in milliseconds).
*/
protected long timeLimit;
/**
* Remember if a retry attempt is undertaken in process()
.
*
* @see #process
*/
protected boolean retrying = false;
/**
* A regular expression describing what to be ignored in the external module's standard error output. Default is
* null
.
*/
protected String ignorePattern = null;
/**
* Get the process object representing the external module program.
*
* @return process
*/
protected Process getProcess() {
return process;
}
protected ExternalModule(String name, String cmd, MaryDataType inputType, MaryDataType outputType, Locale locale)
throws NoSuchPropertyException {
// Exceptions ocurring at this stage should make the program abort.
this.name = name;
this.cmd = cmd;
this.inputType = inputType;
this.outputType = outputType;
this.locale = locale;
this.timeLimit = MaryProperties.needInteger("modules.timeout");
this.requestQueue = new LinkedList();
this.state = MODULE_OFFLINE;
}
/**
* Execute the command cmd
as an external process. The process's input and output streams are accessible from
* then on via the from()
and to()
methods; the process's error stream is logged by a separate
* StreamLogger
thread.
*
* @see #to()
* @see #from()
* @see marytts.util.io.StreamLogger
* @throws IOException
* IOException
*/
protected void open() throws IOException {
assert cmd != null;
process = Runtime.getRuntime().exec(cmd);
// Workaround for Java 1.4.1 bug:
if (System.getProperty("java.vendor").startsWith("Sun") && System.getProperty("java.version").startsWith("1.4.1")) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
}
to = process.getOutputStream();
from = process.getInputStream();
errorLogger = new StreamLogger(process.getErrorStream(), name() + " err", ignorePattern);
errorLogger.start();
}
/**
* Closes the external process's input and output streams, and destroys the process.
*/
protected void close() {
try {
if (to != null)
to.close();
if (from != null)
from.close();
// ErrorLogger will die when it reads end-of-file.
} catch (IOException e) {
}
if (process != null)
process.destroy();
process = null;
to = null;
from = null;
errorLogger = null;
}
/**
* The stream on which data is written to the external process.
*
* @return to
*/
protected OutputStream to() {
return to;
}
/**
* The stream on which data is read from the external process.
*
* @return from
*/
protected InputStream from() {
return from;
}
/**
* The command line to execute as an external process.
*
* @return cmd
*/
protected String cmd() {
return cmd;
}
/**
* Sets the command line to execute.
*
* @param cmd
* cmd
*/
protected void setCmd(String cmd) {
this.cmd = cmd;
}
// Interface MaryModule implementation:
public String name() {
return name;
}
@Deprecated
public MaryDataType inputType() {
return getInputType();
}
public MaryDataType getInputType() {
return inputType;
}
@Deprecated
public MaryDataType outputType() {
return getOutputType();
}
public MaryDataType getOutputType() {
return outputType;
}
public Locale getLocale() {
return locale;
}
public int getState() {
return state;
}
public synchronized void startup() throws Exception {
assert state == MODULE_OFFLINE;
setExitRequested(false);
open();
setNeedToRestart(false);
logger = MaryUtils.getLogger(name());
logger.info("Module started (" + inputType() + "->" + outputType() + ", locale " + getLocale() + ").");
state = MODULE_RUNNING;
}
/**
* Perform a power-on self test by processing some example input data.
*
* @throws Error
* if the module does not work properly.
*/
public synchronized void powerOnSelfTest() throws Error {
assert state == MODULE_RUNNING;
logger.info("Starting power-on self test.");
try {
MaryData in = new MaryData(inputType, getLocale());
String example = inputType.exampleText(getLocale());
if (example != null) {
in.readFrom(new StringReader(example));
if (outputType.equals(MaryDataType.get("AUDIO")))
in.setAudioFileFormat(new AudioFileFormat(AudioFileFormat.Type.WAVE, Voice.AF22050, AudioSystem.NOT_SPECIFIED));
process(in);
} else {
logger.debug("No example text -- no power-on self test!");
}
} catch (Throwable t) {
throw new Error("Module " + name + ": Power-on self test failed.", t);
}
logger.info("Power-on self test complete.");
}
public void shutdown() {
assert state == MODULE_RUNNING;
close();
setExitRequested(true);
doNotifyAll();
try {
processingThread.join();
restarterThread.join();
} catch (InterruptedException e) {
logger.info(e);
}
logger.info("Module shut down.");
state = MODULE_OFFLINE;
}
/**
* The actual external input and output. Write to the module and read from the module in the appropriate ways as determined by
* input and output data types.
*
* @param d
* d
* @throws TransformerConfigurationException
* TransformerConfigurationException
* @throws TransformerException
* TransformerException
* @throws FileNotFoundException
* FileNotFoundException
* @throws IOException
* IOException
* @throws ParserConfigurationException
* ParserConfigurationException
* @throws SAXException
* SAXException
* @throws Exception
* Exception
* @return result
*/
protected MaryData externalIO(MaryData d) throws TransformerConfigurationException, TransformerException,
FileNotFoundException, IOException, ParserConfigurationException, SAXException, Exception {
assert !needToRestart();
logger.info("Writing to module.");
d.writeTo(to());
// Read from external module
logger.info("Reading from module.");
MaryData result = new MaryData(outputType(), d.getLocale());
result.readFrom(from(), outputType().endMarker());
logger.info("Read complete.");
return result;
}
/**
* Feed the input data into the external module, and return the result. This method is responsible for the timer and timeout
* handling and is regarded as generic for all external modules, thus final
. The actual input and output is
* performed by externalIO()
and may be overridden by subclasses to account for module-specifics.
*
* If timeout occurs, the external module is restarted, and a second attempt is made. If it fails again, an IOException is
* thrown. Even in case of the second failure, the external process is restarted, because failure may have been provoked by
* this particular input.
*
* For the time being, external modules are thread-safe simply by this method being synchronized
.
*
* @return A MaryData object of type outputType()
encapsulating the processing result.
*/
public final MaryData process(MaryData d) throws TransformerConfigurationException, TransformerException,
FileNotFoundException, IOException, ParserConfigurationException, SAXException, Exception {
assert state == MODULE_RUNNING;
logger.info("Adding request");
ExternalModuleRequest request = new ExternalModuleRequest(d);
addRequest(request);
// In the case that the processor thread is in wait state,
// wake it up:
doNotifyAll();
logger.info("Now waiting for request to be processed");
long tStart = System.currentTimeMillis();
while (!request.problemOccurred() && request.getOutput() == null && System.currentTimeMillis() - tStart < timeLimit) {
doWait(timeLimit);
}
if (request.getOutput() == null) {
if (request.problemOccurred()) {
logger.error("Problem occurred. Rescheduling request.");
} else {
logger.error("Timeout occurred. Requesting module restart and rescheduling request.");
// We trigger the restart of the module:
setNeedToRestart(true);
}
removeRequest(request);
request.setProblemOccurred(false);
addRequest(request);
doNotifyAll();
logger.info("Waiting for request to be processed (2nd try)");
tStart = System.currentTimeMillis();
while (!request.problemOccurred() && request.getOutput() == null && System.currentTimeMillis() - tStart < timeLimit) {
doWait(timeLimit);
}
if (request.getOutput() == null) {
if (request.problemOccurred()) {
logger.error("Problem occurred again. Giving up.");
} else {
logger.error("Timeout occurred again. Requesting module restart, but giving up on this request.");
// We trigger the restart of the module:
setNeedToRestart(true);
}
removeRequest(request);
throw new IOException("Module " + name() + " cannot process.");
}
}
logger.info("Request processed");
/*
* } catch (Exception e) { String reason; if (timer.didDestroy()) { reason = "Timeout"; } else { reason = "Problem"; } //
* Remedy for all Exceptions: try to restart the external module. if (retrying) { // second failure - abort if
* (timer.didDestroy()) { logger.error(reason + " occurred again. Giving up."); } shutdown(); startup(); retrying = false;
* // at least we are now ready for a different call // (problem might have been due to this particular input) throw e; }
* else { // first failure for this processing attempt logger.error(reason + " occurred during I/O with external module. "
* + "Trying to restart module.", e); shutdown(); startup(); timeLimit = 2 * timeLimit; retrying = true; // marker for
* recursive call to process() return process(d); } }
*/
return request.getOutput();
}
protected synchronized void addRequest(Object r) {
requestQueue.addLast(r);
}
protected synchronized ExternalModuleRequest getNextRequest() {
return (ExternalModuleRequest) requestQueue.removeFirst();
}
protected synchronized void removeRequest(ExternalModuleRequest r) {
requestQueue.remove(r);
}
protected synchronized boolean haveWaitingRequests() {
return !requestQueue.isEmpty();
}
protected synchronized void setNeedToRestart(boolean needToRestart) {
this.needToRestart = needToRestart;
}
protected synchronized boolean needToRestart() {
return needToRestart;
}
protected synchronized void doNotifyAll() {
notifyAll();
}
protected synchronized void doWait() {
try {
wait();
} catch (InterruptedException e) {
logger.info(e);
}
}
protected synchronized void doWait(long millis) {
try {
wait(millis);
} catch (InterruptedException e) {
logger.info(e);
}
}
/**
* Tell all helper threads to exit.
*
* @param b
* b
*/
protected synchronized void setExitRequested(boolean b) {
exitRequested = b;
}
protected synchronized boolean exitRequested() {
return exitRequested;
}
public class ProcessingThread extends Thread {
public void run() {
while (!exitRequested()) {
while (needToRestart()) {
// Wait until restarter thread has finished restarting
logger.info("ProcessingThread waiting for restart.");
doWait();
if (!needToRestart())
logger.info("ProcessingThread noticed restart done.");
}
if (haveWaitingRequests()) {
ExternalModuleRequest request = getNextRequest();
logger.info("Now processing next request.");
try {
MaryData output = externalIO(request.getInput());
request.setOutput(output);
doNotifyAll(); // let them know we're done
} catch (Exception e) {
logger.error("Problem occurred during I/O with external module. " + "Requesting module restart.", e);
// Let whoever scheduled this request decide whether
// they want to reschedule it:
request.setProblemOccurred(true);
// We trigger the restart of the module:
setNeedToRestart(true);
doNotifyAll();
}
} else { // wait for something to process
logger.debug("Currently nothing to do, waiting");
doWait();
logger.debug("ProcessingThread was woken up.");
}
}
}
}
public class RestarterThread extends Thread {
protected static final int MAX_RESTART_ATTEMPTS = 3;
public void run() {
// Avoid eternal retries if restarting does not succeed --
// only retry once in a row:
int nrFailuresRestarting = 0;
while (!exitRequested()) {
if (needToRestart()) {
if (nrFailuresRestarting < MAX_RESTART_ATTEMPTS) {
logger.info("Restarting module.");
try {
close();
open();
setNeedToRestart(false);
logger.info("Module restarted");
doNotifyAll();
nrFailuresRestarting = 0; // succeeded
} catch (Exception e) {
logger.error("Problem restarting.", e);
nrFailuresRestarting++;
}
} else {
logger.error("Restarting has failed " + nrFailuresRestarting + " times, giving up.");
setNeedToRestart(false);
// Maybe it will work again another time?
nrFailuresRestarting = 0;
doNotifyAll();
}
} else { // wait for something to process
logger.info("Currently no need to restart, waiting");
doWait();
logger.info("RestarterThread was woken up.");
}
}
}
}
}