diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java index 5f8df9168..f347eef51 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java @@ -140,7 +140,7 @@ public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioForm } catch(IllegalArgumentException iae) { - LOGGING_SUPPRESSOR.error("no gain control", 5, "Couldn't obtain " + + LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " + "MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + " | " + getChannelName() + "]"); } @@ -152,7 +152,7 @@ public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioForm } catch(IllegalArgumentException iae) { - LOGGING_SUPPRESSOR.error("no mute control", 5, "Couldn't obtain " + + LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " + "MUTE control for stereo line [" + mixer.getMixerInfo().getName() + " | " + getChannelName() + "]"); } @@ -278,7 +278,7 @@ private void playAudio(ByteBuffer buffer) { if(mOutput == null) { - LOGGING_SUPPRESSOR.error("null output", 5, "Audio Output is null - ignoring audio playback request"); + LOGGING_SUPPRESSOR.error("null output", 2, "Audio Output is null - ignoring audio playback request"); return; } @@ -323,8 +323,9 @@ private void playAudio(ByteBuffer buffer) } catch(IllegalArgumentException iae) { - mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" + - mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]"); + LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " + + "MASTER GAIN control for stereo line [" + mMixer.getMixerInfo().getName() + " | " + + getChannelName() + "]"); } try @@ -334,23 +335,24 @@ private void playAudio(ByteBuffer buffer) } catch(IllegalArgumentException iae) { - mLog.warn("Couldn't obtain MUTE control for stereo line [" + - mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]"); + LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " + + "MASTER MUTE control for stereo line [" + mMixer.getMixerInfo().getName() + " | " + + getChannelName() + "]"); } - LOGGING_SUPPRESSOR.info("reopen audio output success", 5, + LOGGING_SUPPRESSOR.info("reopen audio output success", 2, "Closed and reopened audio output - success - mOutput is not null"); } else { - LOGGING_SUPPRESSOR.info("reopen audio output fail", 5, + LOGGING_SUPPRESSOR.info("reopen audio output fail", 2, "Closed and reopened audio output - fail - mOutput is null"); return; } } catch(LineUnavailableException lue) { - LOGGING_SUPPRESSOR.error("reopen fail for lua", 3, "Attempt to reopen " + + LOGGING_SUPPRESSOR.error("reopen fail for lua", 2, "Attempt to reopen " + "audio source data line failed", lue); return; } @@ -393,7 +395,7 @@ private void playAudio(ByteBuffer buffer) } catch(LineUnavailableException e) { - LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 3, + LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 2, "Failed to (re)open the source data line for audio output - mixer [" + mMixer.getMixerInfo().getName() + "]"); return; diff --git a/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java b/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java index e8b28120b..fae2e0327 100644 --- a/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * 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 @@ -35,11 +35,10 @@ import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.heartbeat.Heartbeat; import io.github.dsheirer.source.heartbeat.IHeartbeatListener; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; - public abstract class AbstractChannelState extends Module implements IChannelEventProvider, IDecodeEventProvider, IDecoderStateEventProvider, ISourceEventProvider, IHeartbeatListener, ISquelchStateProvider, IdentifierUpdateProvider, IOverflowListener @@ -53,7 +52,8 @@ public abstract class AbstractChannelState extends Module implements IChannelEve private Channel mChannel; protected boolean mSourceOverflow = false; private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver(); - + protected boolean mTeardownSequenceStarted = false; + protected boolean mTeardownSequenceCompleted = false; //TODO: remove the IOverflowListener code from this class @@ -66,6 +66,22 @@ public AbstractChannelState(Channel channel) mChannel = channel; } + /** + * Indicates if the teardown sequence was started. + */ + public boolean isTeardownSequenceCompleted() + { + return mTeardownSequenceCompleted; + } + + /** + * Indicates if the teardown sequence was completed, meaning that the request disable channel event was dispatched. + */ + public boolean isTeardownSequenceStarted() + { + return mTeardownSequenceStarted; + } + /** * Updates/replaces the current channel configuration with the argument. */ @@ -89,6 +105,11 @@ protected Channel getChannel() */ protected abstract void checkState(); + /** + * Indicates if any timeslot is currently in a TEARDOWN state. + */ + public abstract boolean isTeardownState(); + public abstract List getChannelMetadata(); public abstract void updateChannelStateIdentifiers(IdentifierUpdateNotification notification); diff --git a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java index 8e72c8ab5..05f4da0ed 100644 --- a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * 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 @@ -44,13 +44,12 @@ import io.github.dsheirer.source.SourceType; import io.github.dsheirer.source.config.SourceConfigTuner; import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel @@ -166,6 +165,7 @@ public void stateChanged(State state, int timeslot) mStateMachineMap.get(timeslot).setState(State.IDLE); break; case TEARDOWN: + mTeardownSequenceStarted = true; if(getChannel().isTrafficChannel()) { checkTeardown(); @@ -206,7 +206,16 @@ else if(state == State.TEARDOWN) { if(getChannel().isTrafficChannel()) { - broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE)); + try + { + broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE)); + } + catch(Throwable t) + { + mLog.error("Error broadcasting shutdown channel event", t); + } + + mTeardownSequenceStarted = true; } else { @@ -238,6 +247,20 @@ protected void checkState() } } + @Override + public boolean isTeardownState() + { + for(StateMachine stateMachine: mStateMachineMap.values()) + { + if(stateMachine.getState() == State.TEARDOWN) + { + return true; + } + } + + return false; + } + /** * Creates configuration identifiers for the channel name, system, site and alias list name. */ diff --git a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java index c80e018b4..5d57868f6 100644 --- a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * 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 @@ -46,11 +46,10 @@ import io.github.dsheirer.source.SourceType; import io.github.dsheirer.source.config.SourceConfigTuner; import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Collections; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Channel state tracks the overall state of all processing modules and decoders configured for the channel and @@ -163,9 +162,11 @@ public void stateChanged(State state, int timeslot) mStateMachine.setState(State.IDLE); break; case TEARDOWN: + mTeardownSequenceStarted = true; if(getChannel().isTrafficChannel()) { broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE)); + mTeardownSequenceCompleted = true; } else { @@ -175,6 +176,12 @@ public void stateChanged(State state, int timeslot) } } + @Override + public boolean isTeardownState() + { + return mStateMachine.getState() == State.TEARDOWN; + } + @Override protected void checkState() { diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java index 4e0e2f3a2..3d1dceba6 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java @@ -279,15 +279,27 @@ else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFre break; case REQUEST_DISABLE: case NOTIFICATION_DELETE: - if(channel != null && channel.isProcessing()) + if(channel != null) { try { stopProcessing(channel); } - catch(ChannelException ce) + catch(Throwable t) + { + mLog.error("Error stopping channel [" + channel + "]", t); + } + } + else + { + try { - mLog.error("Error stopping channel [" + channel.getName() + "] - " + ce.getMessage()); + throw new IllegalArgumentException("Request to disable channel must have non-null channel"); + } + catch(IllegalArgumentException iae) + { + mLog.error("Caught a [" + event.getEvent() + "] non-standard channel event - logging stack trace. " + + "This should not happen, please send this error to the developer.", iae); } } break; @@ -521,7 +533,14 @@ else if(request.hasChildDecodeEventHistory()) if(addProcessingChain(channel, processingChain)) { - processingChain.start(); + try + { + processingChain.start(); + } + catch(Throwable t) + { + mLog.error("Error caught during processing chain startup - continuing", t); + } if(GraphicsEnvironment.isHeadless()) { @@ -627,8 +646,28 @@ private void stopProcessing(Channel channel) throws ChannelException } else { - //This has to be done on the FX event thread when the playlist editor is constructed - Platform.runLater(() -> channel.setProcessing(false)); + //When we're in non-headless mode we have to change the processing property on the JavaFX + //event thread. However, if it hasn't yet been initialized (ie an FX window opened), we'll + //get an ISE. In that case, just set the property to false because there won't be any + //property listeners being triggered. + try + { + Platform.runLater(() -> { + try + { + channel.setProcessing(false); + } + catch(Exception e) + { + mLog.error("Error during channel stop while setting processing to false [" + channel + + "] - continuing channel stop process", e); + } + }); + } + catch(IllegalStateException e) + { + channel.setProcessing(false); + } } try @@ -847,6 +886,10 @@ private class ChannelSourceEventErrorListener implements Listener mLog.error("Error stopping channel [" + (toShutdown != null ? toShutdown.getName() : "unknown") + "] with source error - " + ce.getMessage()); } + catch(Throwable t) + { + mLog.error("Error stopping channel [" + toShutdown + "]", t); + } } } } @@ -875,6 +918,8 @@ public String getDiagnosticInformation() sb.append("\tProcessing Chain - Processing: ").append(chain.isProcessing()).append("\n"); AbstractChannelState state = chain.getChannelState(); sb.append("Channel State: ").append(state.getClass()).append("\n"); + sb.append(" Teardown Started:").append(state.isTeardownSequenceStarted()).append("\n"); + sb.append(" Teardown Completed:").append(state.isTeardownSequenceCompleted()).append("\n"); for(ChannelMetadata metadata: state.getChannelMetadata()) { sb.append(metadata.getDescription()).append("\n");