diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadata.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadata.java index ad0dbcfc9..c85579303 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadata.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadata.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.identifier.decoder.ChannelStateIdentifier; import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier; import io.github.dsheirer.sample.Listener; -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 metadata containing details about the channel configuration, decoder state and current @@ -66,17 +65,51 @@ public class ChannelMetadata implements Listener, private AliasModel mAliasModel; private AliasList mAliasList; + /** + * Constructs an instance + * @param aliasModel for alias lookups + * @param timeslot for this metadata + */ public ChannelMetadata(AliasModel aliasModel, Integer timeslot) { mAliasModel = aliasModel; mTimeslot = timeslot; } + /** + * Constructs an instance + * @param aliasModel for alias lookups + */ public ChannelMetadata(AliasModel aliasModel) { this(aliasModel, null); } + /** + * Creates a textual description of this channel metadata. + */ + public String getDescription() + { + StringBuilder sb = new StringBuilder(); + sb.append("Channel Metadata Description\n"); + sb.append("\tTimeslot: ").append(mTimeslot).append("\n"); + Identifier decoder = getDecoderTypeConfigurationIdentifier(); + sb.append("\tDecoder: ").append(decoder != null ? decoder : "(null)").append("\n"); + Identifier state = getChannelStateIdentifier(); + sb.append("\tState: ").append(state != null ? state : "(null)").append("\n"); + Identifier system = getSystemConfigurationIdentifier(); + sb.append("\tSystem: ").append(system != null ? system : "(null)").append("\n"); + Identifier site = getSiteConfigurationIdentifier(); + sb.append("\tSite: ").append(site != null ? site : "(null)").append("\n"); + Identifier channel = getChannelNameConfigurationIdentifier(); + sb.append("\tChannel: ").append(channel != null ? channel : "(null)").append("\n"); + Identifier frequency = getFrequencyConfigurationIdentifier(); + sb.append("\tFrequency: ").append(frequency != null ? frequency : "(null)").append("\n"); + Identifier logical = getDecoderLogicalChannelNameIdentifier(); + sb.append("\tLogical Channel: ").append(logical != null ? logical : "(null)").append("\n"); + return sb.toString(); + } + public Integer getTimeslot() { return mTimeslot; diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java index c9e7a2238..e1a09a0a4 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 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 - * * the Free Software Foundation, either version 3 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, see - * * ***************************************************************************** + * 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 3 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, see + * **************************************************************************** */ package io.github.dsheirer.channel.metadata; @@ -29,16 +26,16 @@ import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier; import io.github.dsheirer.preference.PreferenceType; import io.github.dsheirer.sample.Listener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.table.AbstractTableModel; import java.awt.EventQueue; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.table.AbstractTableModel; public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener { @@ -84,6 +81,47 @@ public void preferenceUpdated(PreferenceType preferenceType) } } + /** + * Creates a diagnostic description of the channel metadata and metadata-channel map. + * @return diagnostic description. + */ + public String getDiagnosticInformation() + { + StringBuilder sb = new StringBuilder(); + Map mapCopy = new HashMap<>(mMetadataChannelMap); + List metadataCopy = new ArrayList<>(mChannelMetadata); + + sb.append("Channel Metadata Model Diagnostics\n"); + + for(ChannelMetadata channelMetadata: metadataCopy) + { + sb.append("\n--------------- CHANNEL METADATA ITEM --------------------\n\n"); + if(mapCopy.containsKey(channelMetadata)) + { + Channel channel = mapCopy.get(channelMetadata); + sb.append("\tMapped Channel:").append(channel).append("\n"); + sb.append("\t\tSource Configuration: ").append(channel.getSourceConfiguration()).append("\n"); + } + else + { + sb.append("\tMapped Channel: **Channel Metadata Is Not In the Metadata:Channel Map**\n"); + } + + sb.append(channelMetadata.getDescription()); + } + + //Check for orphaned map entries that don't match the current channel metadata list. + for(Map.Entry entry: mapCopy.entrySet()) + { + if(!metadataCopy.contains(entry.getKey())) + { + sb.append("\n\tWARNING: orphaned Metadata Channel map entry - channel: " + entry.getValue()).append("\n"); + } + } + + return sb.toString(); + } + public void dispose() { MyEventBus.getGlobalEventBus().unregister(this); 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 e451c9fec..166abf9b5 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java @@ -24,6 +24,7 @@ import io.github.dsheirer.channel.metadata.ChannelAndMetadata; import io.github.dsheirer.channel.metadata.ChannelMetadata; import io.github.dsheirer.channel.metadata.ChannelMetadataModel; +import io.github.dsheirer.channel.state.AbstractChannelState; import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest; import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest; import io.github.dsheirer.controller.channel.event.PreloadDataContent; @@ -52,6 +53,7 @@ import io.github.dsheirer.util.ThreadPool; import java.awt.GraphicsEnvironment; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -72,9 +74,10 @@ */ public class ChannelProcessingManager implements Listener { + private static final String DIVIDER = "-------------------------------------------------------------------------\n"; private final static Logger mLog = LoggerFactory.getLogger(ChannelProcessingManager.class); private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE"; - private Map mProcessingChains = new ConcurrentHashMap<>(); + private Map mProcessingChainsMap = new ConcurrentHashMap<>(); private Lock mLock = new ReentrantLock(); private ChannelSourceEventErrorListener mSourceErrorListener = new ChannelSourceEventErrorListener(); @@ -131,7 +134,7 @@ private boolean isProcessing(Channel channel) try { - isProcessing = mProcessingChains.containsKey(channel) && mProcessingChains.get(channel).isProcessing(); + isProcessing = mProcessingChainsMap.containsKey(channel) && mProcessingChainsMap.get(channel).isProcessing(); } finally { @@ -147,7 +150,7 @@ private boolean isProcessing(Channel channel) */ public boolean isProcessing() { - return !mProcessingChains.isEmpty(); + return !mProcessingChainsMap.isEmpty(); } /** @@ -156,7 +159,7 @@ public boolean isProcessing() */ public ProcessingChain getProcessingChain(Channel channel) { - return mProcessingChains.get(channel); + return mProcessingChainsMap.get(channel); } /** @@ -175,7 +178,7 @@ public Channel getChannel(ProcessingChain processingChain) try { - for(Map.Entry entry : mProcessingChains.entrySet()) + for(Map.Entry entry : mProcessingChainsMap.entrySet()) { if(entry.getValue() == processingChain) { @@ -206,7 +209,7 @@ private Channel getChannel(TunerChannelSource tunerChannelSource) try { - for(Map.Entry entry : mProcessingChains.entrySet()) + for(Map.Entry entry : mProcessingChainsMap.entrySet()) { if(entry.getValue().hasSource(tunerChannelSource)) { @@ -556,10 +559,10 @@ private boolean addProcessingChain(Channel channel, ProcessingChain processingCh try { - if(!mProcessingChains.containsKey(channel)) + if(!mProcessingChainsMap.containsKey(channel)) { added = true; - mProcessingChains.put(channel, processingChain); + mProcessingChainsMap.put(channel, processingChain); getChannelMetadataModel().add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata())); } } @@ -584,7 +587,7 @@ private ProcessingChain removeProcessingChain(Channel channel) try { - removed = mProcessingChains.remove(channel); + removed = mProcessingChainsMap.remove(channel); if(removed != null) { @@ -660,7 +663,7 @@ public void shutdown() mDelayedChannelStartTasks.remove(delayedTask); } - List channelsToStop = new ArrayList<>(mProcessingChains.keySet()); + List channelsToStop = new ArrayList<>(mProcessingChainsMap.keySet()); for(Channel channel : channelsToStop) { @@ -683,7 +686,7 @@ public void shutdown() public void convertToTrafficChannel(ChannelConversionRequest request) { //Update the channel to processing chain map. - ProcessingChain processingChain = mProcessingChains.remove(request.getCurrentChannel()); + ProcessingChain processingChain = mProcessingChainsMap.remove(request.getCurrentChannel()); if(processingChain != null) { @@ -697,7 +700,7 @@ public void convertToTrafficChannel(ChannelConversionRequest request) request.getTrafficChannel().setProcessing(true); }); - mProcessingChains.put(request.getTrafficChannel(), processingChain); + mProcessingChainsMap.put(request.getTrafficChannel(), processingChain); mChannelMetadataModel.updateChannelMetadataToChannelMap(processingChain.getChannelState().getChannelMetadata(), request.getTrafficChannel()); @@ -803,7 +806,7 @@ private class ChannelSourceEventErrorListener implements Listener try { - for(Map.Entry entry: mProcessingChains.entrySet()) + for(Map.Entry entry: mProcessingChainsMap.entrySet()) { if(entry.getValue().hasSource(sourceEvent.getSource())) { @@ -841,4 +844,54 @@ private class ChannelSourceEventErrorListener implements Listener } } } + + /** + * Creates a diagnostic report. + * @return report text. + */ + public String getDiagnosticInformation() + { + StringBuilder sb = new StringBuilder(); + sb.append("Channel Processing Manager - Diagnostics Report\n\n"); + sb.append(DIVIDER); + sb.append("\tChannel to Processing Chain Map Contents\n"); + Map mapCopy = new HashMap<>(mProcessingChainsMap); + for(Map.Entry entry: mapCopy.entrySet()) + { + sb.append("\n\n--------------- CHANNEL:PROCESSING CHAIN MAP ENTRY --------------------\n"); + try + { + Channel channel = entry.getKey(); + sb.append("\tChannel: " + channel).append("\n"); + sb.append("\t\tSource Configuration: " + channel.getSourceConfiguration()).append("\n"); + ProcessingChain chain = entry.getValue(); + sb.append("\tProcessing Chain - Processing: ").append(chain.isProcessing()).append("\n"); + AbstractChannelState state = chain.getChannelState(); + sb.append("Channel State: ").append(state.getClass()).append("\n"); + for(ChannelMetadata metadata: state.getChannelMetadata()) + { + sb.append(metadata.getDescription()).append("\n"); + } + + Source source = chain.getSource(); + + if(source != null) + { + sb.append("Channel Source Class: " + source.getClass()).append("\n"); + sb.append("\t\tTo String:").append(source).append("\n"); + sb.append("\t\tHash:").append(Integer.toHexString(source.hashCode()).toUpperCase()).append("\n"); + } + else + { + sb.append("Channel Source: (null)\n"); + } + } + catch(Throwable t) + { + sb.append("\tError while logging diagnostics of map entry - " + t.getMessage()).append("\n"); + } + } + + return sb.toString(); + } } diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java index 63ed59630..a25b0ad74 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/PolyphaseChannelManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -147,6 +147,7 @@ public String getStateDescription() sb.append(" REQUESTED CF: ").append(FREQUENCY_FORMAT.format(requestedCenterFrequency / 1E6d)); sb.append(" MIXER:").append(FREQUENCY_FORMAT.format(appliedFrequencyOffset / 1E6d)); sb.append(" | Polyphase Indices: ").append(indexes); + sb.append(" HASH:").append(Integer.toHexString(pcs.hashCode()).toUpperCase()); } return sb.toString(); diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java index 1313571f8..c35be9f54 100644 --- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java +++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -43,6 +43,7 @@ import io.github.dsheirer.log.ApplicationLog; import io.github.dsheirer.map.MapService; import io.github.dsheirer.module.log.EventLogManager; +import io.github.dsheirer.monitor.DiagnosticMonitor; import io.github.dsheirer.monitor.ResourceMonitor; import io.github.dsheirer.playlist.PlaylistManager; import io.github.dsheirer.preference.UserPreferences; @@ -125,6 +126,7 @@ public class SDRTrunk implements Listener private AudioStreamingManager mAudioStreamingManager; private BroadcastStatusPanel mBroadcastStatusPanel; private ControllerPanel mControllerPanel; + private DiagnosticMonitor mDiagnosticMonitor; private IconModel mIconModel = new IconModel(); private PlaylistManager mPlaylistManager; private SettingsManager mSettingsManager; @@ -187,6 +189,9 @@ public SDRTrunk() EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences); mPlaylistManager = new PlaylistManager(mUserPreferences, mTunerManager, aliasModel, eventLogManager, mIconModel); + mDiagnosticMonitor = new DiagnosticMonitor(mUserPreferences, mPlaylistManager.getChannelProcessingManager(), + mTunerManager); + if(!GraphicsEnvironment.isHeadless()) { mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences, mTunerManager, mPlaylistManager); @@ -411,6 +416,46 @@ private void initGUI() JMenu fileMenu = new JMenu("File"); menuBar.add(fileMenu); + JMenuItem processingStatusReportMenuItem = new JMenuItem("Processing Diagnostic Report"); + processingStatusReportMenuItem.addActionListener(e -> { + try + { + Path path = mDiagnosticMonitor.generateProcessingDiagnosticReport(); + + JOptionPane.showMessageDialog(mMainGui, "Report created: " + + path.toString(), "Processing Status Report Created", JOptionPane.INFORMATION_MESSAGE); + } + catch(IOException ioe) + { + mLog.error("Error creating processing status report file", ioe); + JOptionPane.showMessageDialog(mMainGui, "Unable to create report file. Please " + + "see application log for details.", "Processing Status Report Failed", JOptionPane.ERROR_MESSAGE); + } + }); + + JMenuItem threadDumpReportMenuItem = new JMenuItem("Thread Dump Report"); + threadDumpReportMenuItem.addActionListener(e -> { + try + { + Path path = mDiagnosticMonitor.generateThreadDumpReport(); + + JOptionPane.showMessageDialog(mMainGui, "Report created: " + + path.toString(), "Thread Dump Report Created", JOptionPane.INFORMATION_MESSAGE); + } + catch(IOException ioe) + { + mLog.error("Error creating thread dump report file", ioe); + JOptionPane.showMessageDialog(mMainGui, "Unable to create report file. Please " + + "see application log for details.", "Thread Dump Report Failed", JOptionPane.ERROR_MESSAGE); + } + }); + + JMenu diagnosticMenu = new JMenu(("Reports")); + diagnosticMenu.add(processingStatusReportMenuItem); + diagnosticMenu.add(threadDumpReportMenuItem); + fileMenu.add(diagnosticMenu); + fileMenu.add(new JSeparator(JSeparator.HORIZONTAL)); + JMenuItem exitMenu = new JMenuItem("Exit"); exitMenu.addActionListener(event -> { processShutdown(); diff --git a/src/main/java/io/github/dsheirer/log/ApplicationLog.java b/src/main/java/io/github/dsheirer/log/ApplicationLog.java index d04cf97ae..155504d0b 100644 --- a/src/main/java/io/github/dsheirer/log/ApplicationLog.java +++ b/src/main/java/io/github/dsheirer/log/ApplicationLog.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2018 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 @@ -15,7 +14,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ package io.github.dsheirer.log; @@ -30,16 +29,15 @@ import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.preference.PreferenceType; import io.github.dsheirer.preference.UserPreferences; -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.net.URL; import java.nio.file.Path; import java.util.Enumeration; import java.util.jar.Attributes; import java.util.jar.Manifest; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Logback and SLF4j logging implementation. @@ -182,7 +180,11 @@ public void stop() } } - private Attributes findManifestAttributes() { + /** + * Finds the jar manifest attributes + * @return attributes or null. + */ + public Attributes findManifestAttributes() { try { Enumeration resources = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF"); while (resources.hasMoreElements()) { @@ -197,9 +199,12 @@ private Attributes findManifestAttributes() { return null; } } - } catch (Exception ex) { + } + catch (Exception ex) + { return null; } + return null; } } diff --git a/src/main/java/io/github/dsheirer/monitor/DiagnosticMonitor.java b/src/main/java/io/github/dsheirer/monitor/DiagnosticMonitor.java new file mode 100644 index 000000000..045747298 --- /dev/null +++ b/src/main/java/io/github/dsheirer/monitor/DiagnosticMonitor.java @@ -0,0 +1,193 @@ +/* + * ***************************************************************************** + * 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 + * the Free Software Foundation, either version 3 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, see + * **************************************************************************** + */ + +package io.github.dsheirer.monitor; + +import io.github.dsheirer.controller.channel.ChannelProcessingManager; +import io.github.dsheirer.preference.UserPreferences; +import io.github.dsheirer.source.tuner.manager.TunerManager; +import io.github.dsheirer.util.TimeStamp; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for monitoring system components and producing logging reports. + */ +public class DiagnosticMonitor +{ + private static final Logger LOGGER = LoggerFactory.getLogger(DiagnosticMonitor.class); + private static final String DIVIDER = "\n\n=========================================================================\n\n"; + private UserPreferences mUserPreferences; + private ChannelProcessingManager mChannelProcessingManager; + private TunerManager mTunerManager; + + /** + * Constructs an instance + * @param userPreferences for application logging directory lookup. + */ + public DiagnosticMonitor(UserPreferences userPreferences, ChannelProcessingManager channelProcessingManager, + TunerManager tunerManager) + { + mUserPreferences = userPreferences; + mChannelProcessingManager = channelProcessingManager; + mTunerManager = tunerManager; + } + + /** + * Creates a diagnostic report containing state information for channels that are in a processing state. + * @return path for the log file that was created. + */ + public Path generateProcessingDiagnosticReport() throws IOException + { + StringBuilder sb = new StringBuilder(); + sb.append("sdrtrunk Processing Diagnostic Report\n"); + sb.append(DIVIDER); + sb.append(getEnvironmentReport()); + sb.append(DIVIDER); + sb.append(mTunerManager.getDiscoveredTunerModel().getDiagnosticReport()); + sb.append(DIVIDER); + sb.append(mChannelProcessingManager.getDiagnosticInformation()); + sb.append(DIVIDER); + sb.append(mChannelProcessingManager.getChannelMetadataModel().getDiagnosticInformation()); + sb.append(DIVIDER); + sb.append(getThreadDumpReport()); + sb.append(DIVIDER); + + Path logDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog(); + String file = TimeStamp.getFileFormattedDateTime() + "_sdrtrunk_processing_diagnostic_report.log"; + Path output = logDirectory.resolve(file); + Files.write(output, sb.toString().getBytes()); + return output; + } + + /** + * Dumps the current threads to a log file with current date and time to the application log directory. + * @return path to the thread dump log file that was created. + * @throws IOException if there is an issue writing the contents to the log file. + */ + public Path generateThreadDumpReport() throws IOException + { + String report = getThreadDumpReport(); + Path logDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog(); + String file = TimeStamp.getFileFormattedDateTime() + "_sdrtrunk_thread_dump.log"; + Path output = logDirectory.resolve(file); + Files.write(output, report.getBytes()); + return output; + } + + /** + * Creates a thread dump report. + * @return report text. + */ + public String getThreadDumpReport() + { + StringBuilder sb = new StringBuilder(); + sb.append("Thread Dump Report\n\n"); + + for(Map.Entry entry : Thread.getAllStackTraces().entrySet()) + { + sb.append(entry.getKey() + " " + entry.getKey().getState()).append("\n"); + + for(StackTraceElement ste : entry.getValue()) + { + sb.append("\tat " + ste).append("\n"); + } + + sb.append("\n\n"); + } + + return sb.toString(); + } + + /** + * Generates a JVM and application environment report + */ + public String getEnvironmentReport() + { + StringBuilder sb = new StringBuilder(); + sb.append("JVM and Application Environment Report\n"); + Attributes atts = findManifestAttributes(); + if (atts != null) { + sb.append("\nVersion : " + atts.getValue("Implementation-Version")); + sb.append("\nGradle Version : " + atts.getValue("Created-By")); + sb.append("\nBuild Timestamp : " + atts.getValue("Build-Timestamp")); + sb.append("\nBuild-JDK : " + atts.getValue("Build-JDK")); + sb.append("\nBuild OS : " + atts.getValue("Build-OS")); + } + else + { + sb.append("\nApplication: no build information available"); + } + + sb.append("\nHost OS Name: " + System.getProperty("os.name")); + sb.append("\nHost OS Arch: " + System.getProperty("os.arch")); + sb.append("\nHost OS Version: " + System.getProperty("os.version")); + sb.append("\nHost CPU Cores: " + Runtime.getRuntime().availableProcessors()); + sb.append("\nHost Max Java Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().maxMemory())); + sb.append("\nHost Allocated Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().totalMemory())); + sb.append("\nHost Free Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().freeMemory())); + sb.append("\nHost Used Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().totalMemory() - + Runtime.getRuntime().freeMemory())); + sb.append("\nStorage Directories:"); + sb.append("\n Application Root: " + mUserPreferences.getDirectoryPreference().getDirectoryApplicationRoot()); + sb.append("\n Application Log: " + mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog()); + sb.append("\n Event Log: " + mUserPreferences.getDirectoryPreference().getDirectoryEventLog()); + sb.append("\n Playlist: " + mUserPreferences.getDirectoryPreference().getDirectoryPlaylist()); + sb.append("\n Recordings: " + mUserPreferences.getDirectoryPreference().getDirectoryRecording()); + sb.append("\n"); + return sb.toString(); + } + + /** + * Finds the jar manifest attributes + * @return attributes or null. + */ + public Attributes findManifestAttributes() { + try { + Enumeration resources = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF"); + while (resources.hasMoreElements()) { + try { + Manifest manifest = new Manifest(resources.nextElement().openStream()); + Attributes atts = manifest.getMainAttributes(); + Boolean hasTitle = atts.containsValue("sdrtrunk project"); + if (hasTitle) { + return atts; + } + } catch (IOException E) { + return null; + } + } + } + catch (Exception ex) + { + return null; + } + + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/DiscoveredTuner.java b/src/main/java/io/github/dsheirer/source/tuner/manager/DiscoveredTuner.java index 42eb2df18..1cade643e 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/DiscoveredTuner.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/DiscoveredTuner.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -59,13 +59,27 @@ public TunerStatus getTunerStatus() * Logs current state of the tuner */ public void logState() + { + mLog.info(getDiagnosticReport()); + } + + /** + * Generates a state report for this tuner. + * @return + */ + public String getDiagnosticReport() { StringBuilder sb = new StringBuilder(); sb.append("Discovered Tuner: ").append(getId()); + sb.append("\n\tClass:").append(getClass()); if(hasTuner()) { - sb.append("\n\tTuner - Frequency:").append(getTuner().getTunerController().getFrequency()); + sb.append("\n\tTuner Class:").append(getTuner().getClass()); + sb.append("\n\tTuner Controller Class:").append(getTuner().getTunerController().getClass()); + sb.append("\n\tFrequency:").append(getTuner().getTunerController().getFrequency()); + sb.append("\n\tError:").append(getErrorMessage()); + sb.append("\n\tChannel Manager Class:").append(getTuner().getChannelSourceManager().getClass()); sb.append("\n\tChannel Manager:").append(getTuner().getChannelSourceManager().getStateDescription()); } else @@ -73,7 +87,7 @@ public void logState() sb.append("\n\tTuner - no tuner"); } - mLog.info(sb.toString()); + return sb.toString(); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java index b4e856faf..5a290aa17 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/HeterodyneChannelSourceManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -72,6 +72,7 @@ public String getStateDescription() .append("] Frequency [").append(channelSource.getFrequency()) .append("] Mixer [").append(channelSource.getMixerFrequency()) .append("]"); + sb.append(" HASH:").append(Integer.toHexString(channelSource.hashCode()).toUpperCase()); } return sb.toString(); diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java index 336c39c9e..d4ea49017 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -776,16 +776,4 @@ public void stop() } } } - - public static void main(String[] args) - { - mLog.info("Starting ..."); - UserPreferences userPreferences = new UserPreferences(); - TunerManager tunerManager = new TunerManager(userPreferences); - tunerManager.start(); - - while(true); - -// mLog.info("Finished!"); - } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java index b2b9ecc43..69b56e4e5 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java +++ b/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 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 @@ -526,4 +526,23 @@ public String getColumnName(int columnIndex) { return COLUMN_HEADERS[columnIndex]; } + + /** + * Generates a diagnostic report for all discovered tuners. + */ + public String getDiagnosticReport() + { + StringBuilder sb = new StringBuilder(); + sb.append("Discovered Tuner Model Diagnostic Report\n"); + + List tunersCopy = new ArrayList<>(mDiscoveredTuners); + + for(DiscoveredTuner tuner: tunersCopy) + { + sb.append("\n\n--------------- DISCOVERED TUNER --------------------\n\n"); + sb.append(tuner.getDiagnosticReport()).append("\n"); + } + + return sb.toString(); + } } \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/util/TimeStamp.java b/src/main/java/io/github/dsheirer/util/TimeStamp.java index abfbd34e0..85a21c482 100644 --- a/src/main/java/io/github/dsheirer/util/TimeStamp.java +++ b/src/main/java/io/github/dsheirer/util/TimeStamp.java @@ -1,7 +1,6 @@ /* - * ****************************************************************************** - * sdrtrunk - * Copyright (C) 2014-2018 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 @@ -15,7 +14,7 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * ***************************************************************************** + * **************************************************************************** */ package io.github.dsheirer.util; @@ -26,8 +25,9 @@ public class TimeStamp { public static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); public static SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HHmmss"); - public static SimpleDateFormat DATE_WITH_MILLISECONDS_FORMAT = new SimpleDateFormat("HHmmss.SSS"); + public static SimpleDateFormat TIME_WITH_MILLISECONDS_FORMAT = new SimpleDateFormat("HHmmss.SSS"); public static SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); + public static SimpleDateFormat DATE_TIME_FORMAT_FILE = new SimpleDateFormat("yyyyMMdd_HHmmss"); public static SimpleDateFormat DATE_TIME_MILLIS_FORMAT = new SimpleDateFormat("yyyyMMdd HHmmss.SSS"); /** @@ -46,6 +46,44 @@ public static synchronized String getFormattedDate(long timestamp) return DATE_FORMAT.format(new Date(timestamp)); } + /** + * Date time formatted for use in a file. + * @param timestamp to format + * @return format string + */ + public static String getFileFormattedDateTime(long timestamp) + { + return DATE_TIME_FORMAT_FILE.format(new Date(timestamp)); + } + + /** + * Current date and time formatted for use in a file. + * @return format string + */ + public static String getFileFormattedDateTime() + { + return getFileFormattedDateTime(System.currentTimeMillis()); + } + + /** + * Creates formatted date and timestamp + * @param timestamp to format + * @return formatted date and time + */ + public static String getFormattedDateTime(long timestamp) + { + return DATE_TIME_FORMAT.format(new Date(timestamp)); + } + + /** + * Creates formatted date and timestamp using now as the timestamp. + * @return formatted date and time + */ + public static String getFormattedDateTime() + { + return getFormattedDateTime(System.currentTimeMillis()); + } + /** * Returns the current system time formatted as HH:mm:ss */ @@ -67,7 +105,7 @@ public static synchronized String getFormattedTime(long timestamp) */ public static synchronized String getFormattedTimeWithMilliseconds(long timestamp) { - return DATE_WITH_MILLISECONDS_FORMAT.format(new Date(timestamp)); + return TIME_WITH_MILLISECONDS_FORMAT.format(new Date(timestamp)); } /**