Project

General

Profile

tc11180x6_deadframe.p

Sergey Ivanovskiy, 08/15/2026 03:17 AM

Download (10 KB)

 
1
/*------------------------------------------------------------------------------
2
  tc11180x6_deadframe.p
3

    
4
  Regression probe X6 for review finding "oldFocusGone cannot tell 'the frame
5
  died during THIS dispatch' from 'the frame was ALREADY dead'"
6
  (ThinClient.processProgressEvent, #11180b post-2026-08-12).
7

    
8
    boolean oldFocusGone = oldFrame != null && !oldFrame.isAlive();   // <-- post-state only
9

    
10
  Frame.isAlive() is a sticky flag that nothing ever resets, and the value is
11
  sampled AFTER the LEAVE dispatch, so the predicate is also true for a frame
12
  that was destroyed long BEFORE this APPLY arrived.  A dead frame keeps the
13
  focus: destroyFrame() marks a regular visible frame setAlive(false), runs
14
  selectiveDisable() and DEFERS the hide, and the deadFrames state sync flips
15
  liveness with no visibility, enablement or focus change at all.
16

    
17
  Consequence claimed by the review: an ORDINARY APPLY 'ENTRY' issued while the
18
  focus still sits in an already-dead frame takes the frame-teardown
19
  "WITHOUT-steal" arm (src.requestFocus(); evt.consume(); return;) and therefore
20
  skips BOTH sendEntry(frame) (the target FRAME's ENTRY trigger) AND
21
  invokeTriggers(src, .., SE_ENTRY) (the target widget's ON ENTRY).  Trunk fired
22
  both.  Nothing in this probe involves a LEAVE trigger, a deletion mid-LEAVE or
23
  a focus steal - it is plain, everyday 4GL.
24

    
25
  This is the ONLY probe in the suite where the frame dies OUTSIDE a LEAVE
26
  trigger; every other #11180 probe (tc11180d/e/f*, tc11180x1..x5,
27
  test-11180-f/-g) deletes from inside the LEAVE, i.e. the legitimate case.
28

    
29
  Two independent phases, two independent target frames, so a phase that fails
30
  to reach the branch on FWD does not spoil the other:
31

    
32
    Phase A - STATIC frame scoped to an internal procedure.  Returning from the
33
              procedure ends the frame's scope -> markFrames(MarkEntry.DESTROY)
34
              -> destroyFrame().  No LEAVE trigger runs.
35
    Phase B - DYNAMIC frame holding a read-only BROWSE, deleted at MAINLINE
36
              (not from any trigger).  The browse is used because a read-only
37
              browse reliably remains the focus widget after its frame is
38
              destroyed (established by tc11180d_false_steal.p).
39

    
40
  Each phase then issues a plain APPLY 'ENTRY' at a fill-in in a different,
41
  fully alive frame and records whether that frame's and that widget's ENTRY
42
  triggers fired.
43

    
44
  READ (per phase):
45
    * target FRAME ENTRY + target widget ENTRY both present
46
        -> correct (native OE, and FWD once oldFocusGone is a transition test)
47
    * either ENTRY absent, FOCUS reported on the dead frame's widget
48
        -> the reviewed defect reproduces
49

    
50
  MEASURED 2026-08-14 - NEITHER PHASE REACHES THE BRANCH.  Both phases MATCH
51
  native OE (docs/11180b/4gl/tc11180x6.log vs deploy/client/tc11180x6.log, FWD at
52
  revno 16725), because neither idiom actually produces the required state, namely
53
  "focus sitting in a frame that is already dead":
54

    
55
    Phase A - FWD leaves fDoomed ALIVE.  FOCUS is still FILL-IN/fDoom after the
56
              procedure returns AND both target ENTRYs fire; a dead frame would
57
              have made the pre-fix oldFocusGone true and dropped both.  So an
58
              internal-procedure-scoped frame whose widget holds focus is not
59
              marked dead here.
60
    Phase B - FOCUS goes <none/invalid> in BOTH engines after the mainline
61
              DELETE WIDGET, so inFocus/oldFrame are null and oldFocusGone is
62
              false either way.  The tc11180d idiom "a read-only browse survives
63
              as the focus widget after its frame dies" holds only INSIDE a LEAVE
64
              dispatch, not at mainline.
65

    
66
  So the r4 finding "oldFocusGone is a post-state test" remains UNREACHED rather
67
  than refuted, and its proposed fix is a provable no-op on this probe (which is
68
  itself the useful result: no regression).  A probe that settles it must create a
69
  dead frame while a LIVE widget still holds focus; these two routes do not.
70
  See 11180b/11180b_v4_proposals.txt for the backed-out fix.
71

    
72
  RESIDUAL DIVERGENCE this probe DID find, unrelated to the above (see phase B
73
  setup, the APPLY that parks focus in frame B): OE fires fT1 LEAVE then
74
  frmT1 LEAVE; FWD omits the FRAME LEAVE.  Structurally upstream of the reviewed
75
  code - sendLeave(oldFrame, frame) runs before oldFocusGone is computed.
76

    
77
  Expected (native OpenEdge, MEASURED): both phases fire both ENTRY triggers.
78
  Expected (FWD 16725, MEASURED): identical in both measured windows; the only
79
  difference anywhere in the log is the missing frmT1 LEAVE noted above.
80

    
81
  Runs head-less on both systems - the whole path is driven synchronously from
82
  APPLY 'ENTRY'.  Log: tc11180x6.log (client cwd).
83
------------------------------------------------------------------------------*/
84

    
85
DEFINE TEMP-TABLE tt NO-UNDO FIELD nm AS CHARACTER.
86
DEFINE QUERY qB FOR tt SCROLLING.
87

    
88
DEFINE VARIABLE fDoom AS CHARACTER NO-UNDO FORMAT "x(8)" INITIAL "doom".
89
DEFINE VARIABLE fT1   AS CHARACTER NO-UNDO FORMAT "x(8)" INITIAL "tgt1".
90
DEFINE VARIABLE fT2   AS CHARACTER NO-UNDO FORMAT "x(8)" INITIAL "tgt2".
91
DEFINE VARIABLE seq   AS INTEGER   NO-UNDO.
92

    
93
DEFINE VARIABLE hFrmB AS HANDLE NO-UNDO.
94
DEFINE VARIABLE hBrwB AS HANDLE NO-UNDO.
95

    
96
/* the two measured destinations - both alive and untouched throughout */
97
DEFINE FRAME frmT1 fT1
98
   WITH TITLE "FRAME-T1 (phase A target)" AT ROW 10 COLUMN 2 SIZE 44 BY 3 SIDE-LABELS.
99

    
100
DEFINE FRAME frmT2 fT2
101
   WITH TITLE "FRAME-T2 (phase B target)" AT ROW 14 COLUMN 2 SIZE 44 BY 3 SIDE-LABELS.
102

    
103
/* Suppress the "Press SPACE BAR to continue" pause on frame hide. */
104
PAUSE 0 BEFORE-HIDE.
105

    
106
PROCEDURE logit:
107
   DEFINE INPUT PARAMETER m AS CHARACTER NO-UNDO.
108
   seq = seq + 1.
109
   OUTPUT TO "tc11180x6.log" APPEND.
110
   PUT UNFORMATTED STRING(seq, "99") " " m SKIP.
111
   OUTPUT CLOSE.
112
END PROCEDURE.
113

    
114
/* NB FOCUS:NAME is the UNKNOWN value for a dynamic widget that was never given a
115
   NAME, and "text" + ? is ? in 4GL - which silently collapsed the whole line to a
116
   bare "?" in the 2026-08-14 runs.  Guard the concatenation. */
117
PROCEDURE logFocus:
118
   DEFINE INPUT PARAMETER m AS CHARACTER NO-UNDO.
119
   RUN logit(m + (IF VALID-HANDLE(FOCUS)
120
                  THEN STRING(FOCUS:TYPE)
121
                       + "/"
122
                       + (IF FOCUS:NAME = ? THEN "<noname>" ELSE FOCUS:NAME)
123
                  ELSE "<none/invalid>")).
124
END PROCEDURE.
125

    
126
/* ---- phase A: park focus in a frame that dies at SCOPE END --------------- */
127
/* FRAME fDoomed is referenced ONLY inside this internal procedure, so it is
128
   scoped to the procedure and is destroyed when the procedure returns.  No
129
   LEAVE trigger is involved - this is scope-end destruction, the path the
130
   review names (markFrames(MarkEntry.DESTROY) -> destroyFrame). */
131
PROCEDURE parkInDoomedFrame:
132
   DISPLAY fDoom WITH FRAME fDoomed
133
      TITLE "FRAME-DOOMED (dies at scope end)" AT ROW 2 COLUMN 2 SIZE 44 BY 3 SIDE-LABELS.
134
   ENABLE fDoom WITH FRAME fDoomed.
135
   APPLY "ENTRY" TO fDoom IN FRAME fDoomed.
136
   RUN logFocus("   phase A: focus parked, procedure about to return; FOCUS = ").
137
END PROCEDURE.
138

    
139
/* ---- target triggers (the measured emits) ------------------------------- */
140
ON ENTRY OF FRAME frmT1
141
   RUN logit("frmT1 ENTRY  <== phase A target FRAME ENTRY (MUST fire)").
142
ON LEAVE OF FRAME frmT1
143
   RUN logit("frmT1 LEAVE").
144
ON ENTRY OF fT1 IN FRAME frmT1
145
   RUN logit("fT1   ENTRY  <== phase A target widget ENTRY (MUST fire; dropped by bug)").
146
ON LEAVE OF fT1 IN FRAME frmT1
147
   RUN logit("fT1   LEAVE").
148

    
149
ON ENTRY OF FRAME frmT2
150
   RUN logit("frmT2 ENTRY  <== phase B target FRAME ENTRY (MUST fire)").
151
ON LEAVE OF FRAME frmT2
152
   RUN logit("frmT2 LEAVE").
153
ON ENTRY OF fT2 IN FRAME frmT2
154
   RUN logit("fT2   ENTRY  <== phase B target widget ENTRY (MUST fire; dropped by bug)").
155
ON LEAVE OF fT2 IN FRAME frmT2
156
   RUN logit("fT2   LEAVE").
157

    
158
/* ---- main --------------------------------------------------------------- */
159
OUTPUT TO "tc11180x6.log".
160
PUT UNFORMATTED "=== tc11180x6_deadframe start ===" SKIP.
161
OUTPUT CLOSE.
162

    
163
CREATE tt. tt.nm = "row1".
164
CREATE tt. tt.nm = "row2".
165
CREATE tt. tt.nm = "row3".
166

    
167
ENABLE fT1 WITH FRAME frmT1.
168
VIEW FRAME frmT1.
169
ENABLE fT2 WITH FRAME frmT2.
170
VIEW FRAME frmT2.
171

    
172
/* ===================== PHASE A - scope-end destruction ==================== */
173
RUN logit("=== PHASE A: static frame destroyed at SCOPE END, then a plain APPLY ===").
174
RUN parkInDoomedFrame.
175
RUN logit("   phase A: parkInDoomedFrame returned - FRAME fDoomed scope has ENDED").
176
RUN logFocus("   phase A: FOCUS after scope end = ").
177

    
178
RUN logit("--- phase A MEASURED WINDOW: APPLY ENTRY to fT1 (ordinary APPLY) ---").
179
APPLY "ENTRY" TO fT1 IN FRAME frmT1.
180
RUN logFocus("--- phase A done; FOCUS = ").
181
RUN logit("--- phase A EXPECT (OE/fixed): frmT1 ENTRY + fT1 ENTRY, FOCUS=FILL-IN/fT1 ---").
182

    
183
/* ===================== PHASE B - mainline DELETE WIDGET =================== */
184
RUN logit("=== PHASE B: dynamic frame + browse deleted at MAINLINE, then a plain APPLY ===").
185

    
186
CREATE FRAME hFrmB
187
   ASSIGN TITLE = "FRAME-B (browse source, deleted at mainline)"
188
          ROW = 6 COLUMN = 2 WIDTH = 34 HEIGHT = 5
189
          BOX = TRUE VISIBLE = FALSE.
190

    
191
OPEN QUERY qB FOR EACH tt.
192

    
193
CREATE BROWSE hBrwB
194
   ASSIGN FRAME = hFrmB
195
          QUERY = QUERY qB:HANDLE
196
          X = 4 Y = 4 WIDTH = 30 DOWN = 2
197
          SEPARATORS = TRUE ROW-MARKERS = FALSE
198
          SENSITIVE = TRUE VISIBLE = TRUE.
199
hBrwB:ADD-COLUMNS-FROM(BUFFER tt:HANDLE).
200
hBrwB:READ-ONLY = TRUE.
201

    
202
hFrmB:VISIBLE = TRUE.
203

    
204
RUN logit("   phase B: APPLY ENTRY to the browse (park focus in frame B)").
205
APPLY "ENTRY" TO hBrwB.
206
RUN logFocus("   phase B: FOCUS parked = ").
207

    
208
/* Delete the FRAME only, from MAINLINE - no trigger, no LEAVE, no steal.
209
   The browse deliberately survives so it remains the focus widget. */
210
RUN logit("   phase B: DELETE WIDGET frame B at mainline (no trigger involved)").
211
IF VALID-HANDLE(hFrmB) THEN DELETE WIDGET hFrmB.
212
RUN logFocus("   phase B: FOCUS after the frame was deleted = ").
213

    
214
RUN logit("--- phase B MEASURED WINDOW: APPLY ENTRY to fT2 (ordinary APPLY) ---").
215
APPLY "ENTRY" TO fT2 IN FRAME frmT2.
216
RUN logFocus("--- phase B done; FOCUS = ").
217
RUN logit("--- phase B EXPECT (OE/fixed): frmT2 ENTRY + fT2 ENTRY, FOCUS=FILL-IN/fT2 ---").
218

    
219
RUN logit("=== tc11180x6_deadframe end ===").
220
QUIT.