Skip to content

Commit 9db5a94

Browse files
andyclaude
andcommitted
Viewer: a failed live property update no longer frees the cached property (#217)
PropertyTask::updateProperty() applies a running job's message to the object getProperty() has cached, and on any failure (a GEOMTRACE step whose size disagrees with its header, a step the data server cannot deliver, an empty value) it deleted that object while p_properties kept pointing at it. The next getProperty("GEOMTRACE") returned freed memory: loadStep() read a garbage step size ("Bad Geomtrace") and then SIGSEGV, the signature of #217; the trace panel crashed in dynamic_cast. Reproduced headless with tests/apps/geomtrace_stress.py (gdb backtrace in __dynamic_cast on the freed property). A failed update now leaves the cached property unchanged, and a parse exception there is caught as getProperty() already does. A second crash found by the same driver: PlaybackControl re-arms a one-shot wxTimer every step and never deleted it, so removing the property panels while a trace animates (a calculation reset, urlStateMCB) let the pending tick reach the destroyed control (SIGSEGV in wxTimerImpl::SendEvent). The destructor now deletes the timer; NModePanel's animation timer the same. GeomTracePropertyPanel: a playback position past the end of a trace that has since changed is clamped instead of collapsing the atoms to the origin, the plot cursor is only set where the plotted series has a point (TEVEC may be shorter than GEOMTRACE), refresh() before initialization and a property without a registry entry no longer dereference null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFPK14wBcLD1s4dYUowpP4
1 parent 8cf4f18 commit 9db5a94

6 files changed

Lines changed: 65 additions & 9 deletions

File tree

‎docs/claude/wx-viewer/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ Read this before changing any dialog, panel, sizer, grid, ewx control or the Ope
3939
- [The Vibrational Frequencies graph is `SpectrumCanvas` over the wx-free `VibSpectrum` model (#214)](vibrational-spectrum-canvas-214.md)
4040
- [Builder panel layouts (View > Panel layout): every pane stays an AUI pane; the one-column modes hide the inactive tab's panes](builder-panel-layouts-one-column.md)
4141
- [A structure opened from a file is not a calculation: `AbstractPropCalculation::getProperty()` throws](structure-file-calculations-throw-on-getproperty.md)
42+
- [A failed live property update must not free the cached property; a panel's timer must die with the panel (#217)](property-cache-and-panel-timers-outlive-their-objects.md)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
type: pitfall
3+
title: "A failed live property update must not free the cached property; a panel's timer must die with the panel (#217)"
4+
area: wx-viewer
5+
paths: ["src/dsm/edsiimpl/PropertyTask.C", "src/wxgui/wxtools/PlaybackControl.C", "src/apps/builder/NModePanel.C", "src/apps/builder/GeomTracePropertyPanel.C", "tests/apps/geomtrace_stress.py"]
6+
issues: [217]
7+
---
8+
**`PropertyTask::updateProperty()` works on the cached object.**
9+
`getProperty()` puts everything it returns into `p_properties`, so the
10+
object a running job's message is applied to is the one every panel and
11+
`loadStep()` will read next. When an update failed (a step whose size
12+
disagrees with its header, a step the data server could not deliver, an
13+
empty value), it used to `delete` that object and leave the cache entry
14+
in place. The next `getProperty("GEOMTRACE")` then returned freed memory:
15+
"Bad Geomtrace" (garbage step size) followed by SIGSEGV, or a crash in
16+
`dynamic_cast`. A failed update now returns 0 and leaves the cached
17+
property as it was.
18+
19+
**Timers owned by a panel are not stopped by wx when the panel goes.**
20+
`PlaybackControl` re-arms a one-shot `wxTimer` for every playback step and
21+
never deleted it; `removePropertyPanels()` (a calculation reset while its
22+
trace or vibration is animating) destroyed the panel and the pending
23+
timer then delivered its tick to freed memory. Delete the timer in the
24+
destructor (`~wxTimer` stops it).
25+
26+
`tests/apps/geomtrace_stress.py` reproduces both (with `--gdb` for a
27+
backtrace); the Builder's `gt...` scene commands are its driver.

‎src/apps/builder/GeomTracePropertyPanel.C‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ wxWindow * GeomTracePropertyPanel::GetTearableContent()
233233

234234
void GeomTracePropertyPanel::refresh()
235235
{
236+
if (!p_playback) return; // not initialized yet
236237
p_playback->SetStopIndex(numSteps()-1);
237238
fillPlot();
238239
}
@@ -369,6 +370,13 @@ void GeomTracePropertyPanel::OnValidation(wxCommandEvent& event)
369370
// we don't want/need undo
370371
void GeomTracePropertyPanel::processStep(int step)
371372
{
373+
// The playback position can outlive the trace it was set for (a reload
374+
// after a message from a running job), and a trace can be empty.
375+
int steps = numSteps();
376+
if (steps == 0) return;
377+
if (step < 0) step = 0;
378+
if (step >= steps) step = steps - 1;
379+
372380
WxVizToolFW& fw = getFW();
373381
IPropCalculation *expt = getCalculation();
374382
SGContainer& sg = fw.getSceneGraph();
@@ -435,7 +443,12 @@ void GeomTracePropertyPanel::processStep(int step)
435443
}
436444
}
437445

438-
p_plotCtrl->SetCursorDataIndex(0, step);
446+
// The plotted series (TEVEC, ...) may have fewer points than the trace.
447+
wxPlotData *curve = p_plotCtrl->CurveIndexOk(0) ?
448+
p_plotCtrl->GetDataCurve(0) : NULL;
449+
if (curve && step < curve->GetCount()) {
450+
p_plotCtrl->SetCursorDataIndex(0, step);
451+
}
439452

440453
// Issue #99: drive the repaint directly instead of relying on Open
441454
// Inventor's change notification, which stops firing after the first
@@ -493,7 +506,7 @@ string GeomTracePropertyPanel::getTracePropKey() const
493506
for (name = names.begin(); name != names.end(); ++name) {
494507
string key = *name;
495508
const Property_Ref *propref = PropFactory::getPropRef(key);
496-
if (propref->classType == TProperty::PROPTSVECTABLE) {
509+
if (propref && propref->classType == TProperty::PROPTSVECTABLE) {
497510
return key;
498511
}
499512
}

‎src/apps/builder/NModePanel.C‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ NModePanel::NModePanel(IPropCalculation *calculation,
128128

129129
NModePanel::~NModePanel()
130130
{
131+
// The animation timer must not fire into a destroyed panel.
132+
delete p_timer;
131133
// The window's canvas calls back into this panel.
132134
if (p_spectrumPop) p_spectrumPop->setClickHandler(0);
133135
if (p_spectrumFrame) p_spectrumFrame->Destroy();

‎src/dsm/edsiimpl/PropertyTask.C‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,12 @@ TProperty* PropertyTask::updateProperty(const string& name, const string& value)
268268
//If property is not on DAV server then return null because this property is
269269
//probably a time - series property that is being cached.
270270
// uhhhh what???
271+
// From here on `property` is the cached object (getProperty() caches
272+
// whatever it returns), so a failed update must leave it alone: deleting
273+
// it left p_properties pointing at freed memory, and the next reader of
274+
// the property -- a geometry step, the trace panel -- crashed (#217).
271275
if (localValue.empty()) {
272-
delete property;
273-
property = NULL;
274-
return property;
276+
return 0;
275277
}
276278

277279

@@ -336,11 +338,15 @@ TProperty* PropertyTask::updateProperty(const string& name, const string& value)
336338
// This may change, but for now, PROPERTY_OUTPUT files are not parsed
337339
PropertyDoc doc;
338340
if (!isOutputFile) {
339-
doc.parse(localValue.c_str(), false);
341+
try {
342+
doc.parse(localValue.c_str(), false);
343+
} catch (...) {
344+
// A step the data server could not deliver leaves the URL here.
345+
return 0;
346+
}
340347
}
341348

342-
if ( (doc.empty() && !isOutputFile) || !property ) {
343-
delete property;
349+
if (doc.empty() && !isOutputFile) {
344350
property = NULL;
345351
}
346352
else {
@@ -373,7 +379,6 @@ TProperty* PropertyTask::updateProperty(const string& name, const string& value)
373379
default: ok = false; break;
374380
};
375381
if (!ok) {
376-
delete property;
377382
property = NULL;
378383
}
379384
else {

‎src/wxgui/wxtools/PlaybackControl.C‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ PlaybackControl::PlaybackControl(wxWindow* parent, wxWindowID id,
106106

107107
PlaybackControl::~PlaybackControl()
108108
{
109+
// A one-shot is pending for as long as playback runs; one that fired
110+
// after the panel was destroyed would deliver its tick to freed memory.
111+
delete p_timer;
112+
if (p_delayInternal) delete p_delay;
113+
if (p_startIndexInternal) delete p_startIndex;
114+
if (p_stopIndexInternal) delete p_stopIndex;
115+
if (p_stepSizeInternal) delete p_stepSize;
116+
if (p_currentStepInternal) delete p_currentStep;
109117
}
110118

111119

0 commit comments

Comments
 (0)