Skip to content

Commit 2064ffc

Browse files
committed
Add file validation and error handling in BinaryRecording and RecordNode
Fixes #679
1 parent 90bc34d commit 2064ffc

File tree

7 files changed

+160
-19
lines changed

7 files changed

+160
-19
lines changed

Source/Processors/RecordNode/BinaryFormat/BinaryRecording.cpp

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "../../Settings/DataStream.h"
2727
#include "../../Settings/InfoObject.h"
2828

29+
#include "../../../CoreServices.h"
2930
#include "../../Events/Spike.h"
3031

3132
#define TIC std::chrono::high_resolution_clock::now()
@@ -95,7 +96,7 @@ void BinaryRecording::openFiles (File rootFolder, int experimentNumber, int reco
9596
if (streamId != lastStreamId)
9697
{
9798
wroteFirstSampleNumber[streamId] = false;
98-
99+
99100
firstChannels.add (channelInfo);
100101
streamIndex++;
101102

@@ -120,7 +121,7 @@ void BinaryRecording::openFiles (File rootFolder, int experimentNumber, int reco
120121
singleChannelJSON->setProperty ("history", channelInfo->getHistoryString());
121122
singleChannelJSON->setProperty ("bit_volts", channelInfo->getBitVolts());
122123
singleChannelJSON->setProperty ("units", channelInfo->getUnits());
123-
singleChannelJSON->setProperty ("type", static_cast<int>(channelInfo->getChannelType()));
124+
singleChannelJSON->setProperty ("type", static_cast<int> (channelInfo->getChannelType()));
124125
createChannelMetadata (channelInfo, singleChannelJSON);
125126

126127
singleStreamJSON.add (var (singleChannelJSON));
@@ -140,11 +141,14 @@ void BinaryRecording::openFiles (File rootFolder, int experimentNumber, int reco
140141
String datPath = getProcessorString (ch);
141142
String filename = contPath + datPath + "continuous.dat";
142143

143-
LOGD ("Creating file: ", contPath, datPath, "sample_numbers.npy");
144-
ScopedPointer<NpyFile> tFile = new NpyFile (contPath + datPath + "sample_numbers.npy", NpyType (BaseType::INT64, 1));
144+
String samplesPath = contPath + datPath + "sample_numbers.npy";
145+
LOGD ("Creating file: ", samplesPath);
146+
ScopedPointer<NpyFile> tFile = new NpyFile (samplesPath, NpyType (BaseType::INT64, 1));
145147
m_dataTimestampFiles.add (tFile.release());
146148

147-
ScopedPointer<NpyFile> syncTimestampFile = new NpyFile (contPath + datPath + "timestamps.npy", NpyType (BaseType::DOUBLE, 1));
149+
String syncTimestampPath = contPath + datPath + "timestamps.npy";
150+
LOGD ("Creating file: ", syncTimestampPath);
151+
ScopedPointer<NpyFile> syncTimestampFile = new NpyFile (syncTimestampPath, NpyType (BaseType::DOUBLE, 1));
148152
m_dataSyncTimestampFiles.add (syncTimestampFile.release());
149153

150154
DynamicObject::Ptr fileJSON = new DynamicObject();
@@ -332,7 +336,113 @@ void BinaryRecording::openFiles (File rootFolder, int experimentNumber, int reco
332336

333337
settingsJSON->writeAsJSON (settingsFileStream, JSON::FormatOptions {}.withIndentLevel (2).withSpacing (JSON::Spacing::multiLine).withMaxDecimalPlaces (10));
334338

335-
339+
// Validate that all files were opened successfully
340+
if (! validateOpenFiles())
341+
{
342+
String errorMsg = "Recording stopped! Failed to open one or more recording files. Please check disk space and write permissions.";
343+
344+
LOGE ("BinaryRecording::openFiles: ", errorMsg);
345+
346+
// Stop recording and show error message on the message thread
347+
CoreServices::setRecordingStatus (false);
348+
MessageManager::callAsync ([]
349+
{ CoreServices::sendStatusMessage ("Unable to start recording. Please check the console for errors."); });
350+
}
351+
}
352+
353+
bool BinaryRecording::validateOpenFiles() const
354+
{
355+
bool allFilesValid = true;
356+
357+
// Check continuous data files (SequentialBlockFile)
358+
for (int i = 0; i < m_continuousFiles.size(); i++)
359+
{
360+
if (m_continuousFiles[i] == nullptr)
361+
{
362+
allFilesValid = false;
363+
}
364+
}
365+
366+
// Check timestamp files
367+
for (int i = 0; i < m_dataTimestampFiles.size(); i++)
368+
{
369+
if (m_dataTimestampFiles[i] == nullptr || ! m_dataTimestampFiles[i]->isOpen())
370+
{
371+
allFilesValid = false;
372+
}
373+
}
374+
375+
// Check sync timestamp files
376+
for (int i = 0; i < m_dataSyncTimestampFiles.size(); i++)
377+
{
378+
if (m_dataSyncTimestampFiles[i] == nullptr || ! m_dataSyncTimestampFiles[i]->isOpen())
379+
{
380+
allFilesValid = false;
381+
}
382+
}
383+
384+
// Check event files
385+
for (int i = 0; i < m_eventFiles.size(); i++)
386+
{
387+
EventRecording* rec = m_eventFiles[i];
388+
if (rec != nullptr)
389+
{
390+
if (rec->data == nullptr || ! rec->data->isOpen())
391+
{
392+
allFilesValid = false;
393+
}
394+
if (rec->samples == nullptr || ! rec->samples->isOpen())
395+
{
396+
allFilesValid = false;
397+
}
398+
if (rec->timestamps == nullptr || ! rec->timestamps->isOpen())
399+
{
400+
allFilesValid = false;
401+
}
402+
// extraFile is optional (only for TTL full words)
403+
if (rec->extraFile != nullptr && ! rec->extraFile->isOpen())
404+
{
405+
allFilesValid = false;
406+
}
407+
}
408+
}
409+
410+
// Check spike files
411+
for (int i = 0; i < m_spikeFiles.size(); i++)
412+
{
413+
EventRecording* rec = m_spikeFiles[i];
414+
if (rec != nullptr)
415+
{
416+
if (rec->data == nullptr || ! rec->data->isOpen())
417+
{
418+
allFilesValid = false;
419+
}
420+
if (rec->samples == nullptr || ! rec->samples->isOpen())
421+
{
422+
allFilesValid = false;
423+
}
424+
if (rec->timestamps == nullptr || ! rec->timestamps->isOpen())
425+
{
426+
allFilesValid = false;
427+
}
428+
if (rec->channels == nullptr || ! rec->channels->isOpen())
429+
{
430+
allFilesValid = false;
431+
}
432+
if (rec->extraFile == nullptr || ! rec->extraFile->isOpen())
433+
{
434+
allFilesValid = false;
435+
}
436+
}
437+
}
438+
439+
// Check sync text file
440+
if (m_syncTextFile == nullptr)
441+
{
442+
allFilesValid = false;
443+
}
444+
445+
return allFilesValid;
336446
}
337447

338448
std::unique_ptr<NpyFile> BinaryRecording::createEventMetadataFile (const MetadataEventObject* channel, String filename, DynamicObject* jsonFile)
@@ -549,6 +659,9 @@ void BinaryRecording::writeContinuousData (int writeChannel,
549659
/* Get the file index that belongs to the current recording channel */
550660
int fileIndex = m_fileIndexes[writeChannel];
551661

662+
if (! m_continuousFiles[fileIndex])
663+
return;
664+
552665
/* Write the data to that file */
553666
m_continuousFiles[fileIndex]->writeChannel (
554667
m_samplesWritten[writeChannel],
@@ -565,7 +678,7 @@ void BinaryRecording::writeContinuousData (int writeChannel,
565678

566679
uint32 streamId = getContinuousChannel (realChannel)->getStreamId();
567680

568-
if (! wroteFirstSampleNumber[streamId] )
681+
if (! wroteFirstSampleNumber[streamId])
569682
{
570683
firstSampleNumber[streamId] = baseSampleNumber;
571684
wroteFirstSampleNumber[streamId] = true;
@@ -682,11 +795,11 @@ void BinaryRecording::writeTimestampSyncText (uint64 streamId, int64 sampleNumbe
682795

683796
int64 fsn = firstSampleNumber[streamId];
684797

685-
if(streamId > 0)
798+
if (streamId > 0)
686799
jassert (fsn == sampleNumber);
687800

688801
m_syncTextFile->writeText (syncString + "\r\n", false, false, nullptr);
689-
802+
690803
m_syncTextFile->flush();
691804
}
692805

Source/Processors/RecordNode/BinaryFormat/BinaryRecording.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ class BinaryRecording : public RecordEngine
8989
void createChannelMetadata (const MetadataObject* channel, DynamicObject* jsonObject);
9090
void writeEventMetadata (const MetadataEvent* event, NpyFile* file);
9191
void increaseEventCounts (EventRecording* rec);
92+
93+
/** Validates that all recording files were opened successfully.
94+
Returns true if all files are valid, false otherwise. */
95+
bool validateOpenFiles () const;
9296

9397
bool m_saveTTLWords { true };
9498

Source/Processors/RecordNode/BinaryFormat/NpyFile.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ bool NpyFile::openFile (String path)
7070
Result res = file.create();
7171
if (res.failed())
7272
{
73-
std::cerr << "Error creating file " << path << ":" << res.getErrorMessage() << std::endl;
73+
LOGD ("Error creating file ", path, ": ", res.getErrorMessage());
7474
file.deleteFile();
7575
Result res = file.create();
7676
LOGD ("Re-creating file: ", path);
@@ -108,7 +108,7 @@ void NpyFile::writeHeader (const Array<NpyType>& typeList)
108108
String magicStr = "NUMPY";
109109
uint16 ver = 0x0001;
110110
// magic = magic number + magic string + magic version
111-
int magicLen = int( sizeof (uint8) + magicStr.getNumBytesAsUTF8() + sizeof (uint16));
111+
int magicLen = int (sizeof (uint8) + magicStr.getNumBytesAsUTF8() + sizeof (uint16));
112112
int nbytesAlign = 64; // header should use an integer multiple of this many bytes
113113

114114
bool multiValue = typeList.size() > 1;
@@ -157,7 +157,7 @@ void NpyFile::writeHeader (const Array<NpyType>& typeList)
157157

158158
void NpyFile::updateHeader()
159159
{
160-
if (true)
160+
if (m_okOpen) // only update if file opened successfully
161161
{
162162
// overwrite the shape part of the header - even without explicitly calling
163163
// m_file->flush(), overwriting seems to trigger a flush to disk,
@@ -251,7 +251,7 @@ int NpyType::getTypeLength() const
251251
if (type == BaseType::CHAR)
252252
return 1;
253253
else
254-
return int(length);
254+
return int (length);
255255
}
256256

257257
String NpyType::getName() const

Source/Processors/RecordNode/BinaryFormat/NpyFile.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ class PLUGIN_API NpyFile
9999
/** Increases the count of the number of records in the file (must match the number of samples written) */
100100
void increaseRecordCount (int count = 1);
101101

102+
/** Returns true if the file was opened successfully */
103+
bool isOpen() const { return m_okOpen; }
104+
102105
private:
103106
/** Opens the file at a specified path */
104107
bool openFile (String path);

Source/Processors/RecordNode/BinaryFormat/SequentialBlockFile.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ SequentialBlockFile::~SequentialBlockFile()
4444
}
4545

4646
//manually flush the last one to avoid trailing zeroes
47-
m_memBlocks[0]->partialFlush (m_lastBlockFill * m_nChannels);
47+
if (m_memBlocks.size() > 0)
48+
m_memBlocks[0]->partialFlush (m_lastBlockFill * m_nChannels);
4849
}
4950

5051
bool SequentialBlockFile::openFile (String filename)
@@ -53,7 +54,7 @@ bool SequentialBlockFile::openFile (String filename)
5354
Result res = file.create();
5455
if (res.failed())
5556
{
56-
std::cerr << "Error creating file " << filename << ":" << res.getErrorMessage() << std::endl;
57+
LOGD ("Error creating file ", filename, ": ", res.getErrorMessage());
5758
file.deleteFile();
5859
Result res = file.create();
5960
LOGD ("Re-creating file: ", filename);
@@ -104,7 +105,7 @@ bool SequentialBlockFile::writeChannel (uint64 startPos, int channel, int16* dat
104105
while (writtenSamples < nSamples)
105106
{
106107
int16* blockPtr = m_memBlocks[bIndex]->getData();
107-
int samplesToWrite = jmin ((nSamples - writtenSamples), (m_samplesPerBlock - int(startIdx)));
108+
int samplesToWrite = jmin ((nSamples - writtenSamples), (m_samplesPerBlock - int (startIdx)));
108109

109110
for (int i = 0; i < samplesToWrite; i++)
110111
{

Source/Processors/RecordNode/RecordNode.cpp

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,24 @@ void RecordNode::startRecording()
865865

866866
if (! rootFolder.exists())
867867
{
868-
rootFolder.createDirectory();
868+
Result res = rootFolder.createDirectory();
869+
if (res.failed())
870+
{
871+
LOGE ("Record Node " + String (getNodeId()) + ": Could not create directory: " + rootFolder.getFullPathName(), " -- ", res.getErrorMessage());
872+
873+
CoreServices::setRecordingStatus (false);
874+
875+
if (! headlessMode)
876+
{
877+
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
878+
"Recording Error",
879+
"Record Node " + String (getNodeId()) + " - Could not create recording directory:\n\n"
880+
+ rootFolder.getFullPathName() + "\n\n"
881+
+ res.getErrorMessage());
882+
}
883+
884+
return;
885+
}
869886
}
870887

871888
recordThread->setFileComponents (rootFolder, experimentNumber, recordingNumber);
@@ -889,6 +906,9 @@ void RecordNode::startRecording()
889906
// called by GenericProcessor::setRecording() and CoreServices::setRecordingStatus()
890907
void RecordNode::stopRecording()
891908
{
909+
if (! isRecording)
910+
return;
911+
892912
isRecording = false;
893913
hasRecorded = true;
894914
recordingNumber++; // increment recording number within this directory; should be zero for first recording

Source/UI/ControlPanel.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,10 +1048,10 @@ void ControlPanel::startRecording()
10481048

10491049
filenameComponent->setEnabled (false);
10501050

1051-
graph->setRecordState (true);
1052-
10531051
LOGC ("Starting recording");
10541052

1053+
graph->setRecordState (true);
1054+
10551055
repaint();
10561056
}
10571057

0 commit comments

Comments
 (0)