ntk 8.3.0 → 8.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/app.js +7 -0
- package/lib/cleanup.js +23 -7
- package/lib/window.js +103 -25
- package/package.json +1 -1
package/lib/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectionGone } from './cleanup.js';
|
|
1
2
|
import Clipboard from './clipboard.js';
|
|
2
3
|
import { CursorCache } from './cursor.js';
|
|
3
4
|
import { GLError, backendFor, glCapabilities, glError, resolveGLPolicy } from './gl.js';
|
|
@@ -253,6 +254,12 @@ export default class App {
|
|
|
253
254
|
_probeRefreshRate() {
|
|
254
255
|
this._refreshProbe = null; // in progress; only ever started once
|
|
255
256
|
const X = this.X;
|
|
257
|
+
// A connection on its way out is one more "no rate to be had". The probe
|
|
258
|
+
// is started lazily by the first window built on this connection, which
|
|
259
|
+
// may be one adopted from an event still arriving as it closes — and
|
|
260
|
+
// node-x11 throws synchronously at a request from then on, where the
|
|
261
|
+
// event dispatch has no caller to catch it (issue #321).
|
|
262
|
+
if (connectionGone(X)) return;
|
|
256
263
|
X.require('randr', (err, R) => {
|
|
257
264
|
if (err || !R) return;
|
|
258
265
|
const root = this.display.screen[0].root;
|
package/lib/cleanup.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Whether a request issued now would throw rather than reach the server.
|
|
3
|
+
* node-x11 throws *synchronously* ("client is in closing state") once
|
|
4
|
+
* `close()` has begun, and a destroyed or ended stream is the same state one
|
|
5
|
+
* step further along.
|
|
6
|
+
*
|
|
7
|
+
* The check matters wherever a request is issued from somewhere with no
|
|
8
|
+
* caller to catch: a FinalizationRegistry callback, a paced frame's timer, or
|
|
9
|
+
* the X event dispatch itself — events already in the read buffer keep being
|
|
10
|
+
* delivered after `close()` (issue #321).
|
|
11
|
+
*/
|
|
12
|
+
export function connectionGone(X) {
|
|
13
|
+
return !!(X._closing || !X.stream || X.stream.destroyed || X.stream.writableEnded);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Issue requests — typically releasing a server-side resource — tolerating a
|
|
17
|
+
// connection that is closing or already gone. The X server frees all of a
|
|
18
|
+
// client's resources on disconnect, so there is nothing left to do and
|
|
19
|
+
// nothing worth throwing about. This matters wherever the requests have no
|
|
20
|
+
// caller around them to catch: FinalizationRegistry callbacks, which run
|
|
21
|
+
// after app.close() if wrappers get garbage collected late, a paced frame's
|
|
22
|
+
// timer, and the X event dispatch (issue #321).
|
|
7
23
|
export function safeRelease(X, fn) {
|
|
8
|
-
if (X
|
|
24
|
+
if (connectionGone(X)) return;
|
|
9
25
|
try {
|
|
10
26
|
fn();
|
|
11
27
|
} catch {
|
package/lib/window.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import x11 from 'x11';
|
|
2
2
|
|
|
3
3
|
import { builtin } from './builtin.js';
|
|
4
|
-
import { safeRelease } from './cleanup.js';
|
|
4
|
+
import { connectionGone, safeRelease } from './cleanup.js';
|
|
5
5
|
import Drawable from './drawable.js';
|
|
6
6
|
import { packIcons, unpackIcons } from './imagedata.js';
|
|
7
7
|
import Pixmap from './pixmap.js';
|
|
@@ -606,22 +606,34 @@ export default class Window extends Drawable {
|
|
|
606
606
|
// client chose the visual for (issue #295). They go out in the same
|
|
607
607
|
// batch, so the pair costs one round trip, not two, and `ready` is the
|
|
608
608
|
// wait for both.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
609
|
+
//
|
|
610
|
+
// Nothing is asked of a connection that is going away: node-x11 throws
|
|
611
|
+
// synchronously at a request once close() has begun, and adoption
|
|
612
|
+
// happens from places with no caller to catch — a routed child-event,
|
|
613
|
+
// XEmbed, the shared glyph cache all build windows from inside the X
|
|
614
|
+
// event dispatch (issue #321). There is nothing to learn there either,
|
|
615
|
+
// so `ready` settles with the geometry unknown, the same outcome as a
|
|
616
|
+
// window destroyed before its reply landed (see the getter).
|
|
617
|
+
this._readyPending = connectionGone(X) ? 0 : 2;
|
|
618
|
+
if (this._readyPending) {
|
|
619
|
+
X.GetGeometry(this.id, (err, res) => {
|
|
620
|
+
// A window that was destroyed between the id reaching us and this
|
|
621
|
+
// request reaching the server answers BadWindow, and there is
|
|
622
|
+
// nothing to record. `ready` still resolves: adopting a window that
|
|
623
|
+
// has since gone is ordinary for a window manager, and a wait that
|
|
624
|
+
// never ends is worse than one that ends with width undefined.
|
|
625
|
+
this._readyPending--;
|
|
626
|
+
if (err) this._settleReady();
|
|
627
|
+
else this._applyGeometry(unpackGeometry(res));
|
|
628
|
+
});
|
|
629
|
+
X.GetWindowAttributes(this.id, (err, attrs) => {
|
|
630
|
+
this._readyPending--;
|
|
631
|
+
if (!err) this.visualId = attrs.visual;
|
|
632
|
+
this._settleReady();
|
|
633
|
+
});
|
|
634
|
+
} else {
|
|
623
635
|
this._settleReady();
|
|
624
|
-
}
|
|
636
|
+
}
|
|
625
637
|
}
|
|
626
638
|
|
|
627
639
|
Window._cacheFor(app).set(this.id, this);
|
|
@@ -779,6 +791,12 @@ export default class Window extends Drawable {
|
|
|
779
791
|
this.on('child-event', (ev) => {
|
|
780
792
|
const eventName = xevents.eventName[ev.type];
|
|
781
793
|
if (!eventName) return;
|
|
794
|
+
// Events buffered before `close()` keep being dispatched after it, and
|
|
795
|
+
// a window this client already saw destroyed has no substructure left
|
|
796
|
+
// to report. Neither is worth building a child for — and the questions
|
|
797
|
+
// the constructor would ask on the way out are what used to throw into
|
|
798
|
+
// the dispatcher (issue #321).
|
|
799
|
+
if (this._destroyed || connectionGone(X)) return;
|
|
782
800
|
const child = new Window(app, { id: ev.wid });
|
|
783
801
|
const ntkev = { ...ev, parent: this, window: child, target: child };
|
|
784
802
|
// wait until we know that we track correct x,y,w,h values
|
|
@@ -788,6 +806,9 @@ export default class Window extends Drawable {
|
|
|
788
806
|
});
|
|
789
807
|
|
|
790
808
|
this.on('newListener', (name) => {
|
|
809
|
+
// one of ntk's own handlers going on, not a caller asking for an
|
|
810
|
+
// event: nothing to arm and nothing to select — see _listenInternal
|
|
811
|
+
if (this._internalListen) return;
|
|
791
812
|
// Listening for 'close' is the opt-in. WM_DELETE_WINDOW only reaches a
|
|
792
813
|
// client that advertised it in WM_PROTOCOLS — a window manager kills
|
|
793
814
|
// anyone else outright — and having to know that, on top of decoding a
|
|
@@ -814,6 +835,13 @@ export default class Window extends Drawable {
|
|
|
814
835
|
const eventMask = xevents.mask[name];
|
|
815
836
|
if (!eventMask) return;
|
|
816
837
|
if ((eventMask & this.eventMask) === 0) {
|
|
838
|
+
// A connection on its way out can select nothing, and node-x11 throws
|
|
839
|
+
// synchronously at a request from then on. This hook runs from
|
|
840
|
+
// `.on()` — including the calls made while adopting a window from
|
|
841
|
+
// inside the X event dispatch, where there is no caller to catch it
|
|
842
|
+
// (issue #321). Before the mask is touched, so it goes on saying what
|
|
843
|
+
// this connection holds.
|
|
844
|
+
if (connectionGone(X)) return;
|
|
817
845
|
this.eventMask |= eventMask;
|
|
818
846
|
// the selection can legitimately fail — SubstructureRedirect is
|
|
819
847
|
// one-client-only, so `on('map_request')` is how you find out
|
|
@@ -821,14 +849,29 @@ export default class Window extends Drawable {
|
|
|
821
849
|
// error hook rather than dropping it; selectInput() is the
|
|
822
850
|
// explicit form that hands the error straight back.
|
|
823
851
|
X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) => {
|
|
824
|
-
if (err)
|
|
852
|
+
if (!err) return;
|
|
853
|
+
// the request failed as a whole, so the server kept the mask it
|
|
854
|
+
// had: drop the bits again rather than leave `eventMask` claiming
|
|
855
|
+
// a selection this connection does not hold. selectInput() trusts
|
|
856
|
+
// it to decide whether a write can be skipped.
|
|
857
|
+
this.eventMask &= ~eventMask;
|
|
858
|
+
this.app.options.onXError?.(err);
|
|
825
859
|
});
|
|
826
860
|
}
|
|
827
861
|
});
|
|
828
862
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
863
|
+
// ntk's own bookkeeping listens for the same events an app does, and the
|
|
864
|
+
// hook above reads a listener as a caller expressing interest and selects
|
|
865
|
+
// for it. For a window ntk created that is free — StructureNotify is in
|
|
866
|
+
// the CreateWindow value list already — but for an adopted one the
|
|
867
|
+
// tracked mask starts at zero and the write is absolute, so it would
|
|
868
|
+
// replace whatever this connection had selected on a window another
|
|
869
|
+
// client owns (issue #322). Subscribe past the hook: adopting a window
|
|
870
|
+
// costs no requests and changes no server-side state, and a caller that
|
|
871
|
+
// wants these events still asks for them, with on('map') or selectInput.
|
|
872
|
+
this._listenInternal('map', () => (this._mapped = true));
|
|
873
|
+
this._listenInternal('unmap', () => (this._mapped = false));
|
|
874
|
+
this._listenInternal('destroy', () => {
|
|
832
875
|
this._destroyed = true;
|
|
833
876
|
this._forget();
|
|
834
877
|
this._teardownFrame(false); // the server has already taken the window
|
|
@@ -842,6 +885,24 @@ export default class Window extends Drawable {
|
|
|
842
885
|
}
|
|
843
886
|
}
|
|
844
887
|
|
|
888
|
+
/**
|
|
889
|
+
* Attach one of ntk's own handlers without the `newListener` hook reading
|
|
890
|
+
* it as a caller asking for the event. The hook's job is to turn expressed
|
|
891
|
+
* interest into a server-side selection; ntk's internal bookkeeping
|
|
892
|
+
* expresses none — it only wants the events that arrive anyway — and on an
|
|
893
|
+
* adopted window a selection nobody asked for overwrites another
|
|
894
|
+
* subsystem's (issue #322). The flag is read synchronously by the hook,
|
|
895
|
+
* which EventEmitter emits before the listener is stored.
|
|
896
|
+
*/
|
|
897
|
+
_listenInternal(name, fn) {
|
|
898
|
+
this._internalListen = true;
|
|
899
|
+
try {
|
|
900
|
+
this.on(name, fn);
|
|
901
|
+
} finally {
|
|
902
|
+
this._internalListen = false;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
845
906
|
/**
|
|
846
907
|
* Resolves with this window once its geometry is known — immediately for a
|
|
847
908
|
* window ntk created (it already knows what it asked for), when the
|
|
@@ -1098,7 +1159,11 @@ export default class Window extends Drawable {
|
|
|
1098
1159
|
|
|
1099
1160
|
_handleExpose(ev) {
|
|
1100
1161
|
if (this._backingValid) {
|
|
1101
|
-
|
|
1162
|
+
// an expose can arrive after the connection started closing, and this
|
|
1163
|
+
// runs inside the event dispatch where a throw has no caller (#321)
|
|
1164
|
+
safeRelease(this.X, () => {
|
|
1165
|
+
this.X.CopyArea(this._backing.id, this.id, this._presentGc, ev.x, ev.y, ev.x, ev.y, ev.width, ev.height);
|
|
1166
|
+
});
|
|
1102
1167
|
} else {
|
|
1103
1168
|
this._requestRedraw();
|
|
1104
1169
|
}
|
|
@@ -3517,13 +3582,26 @@ export default class Window extends Drawable {
|
|
|
3517
3582
|
* either makes you the window manager or rejects with BadAccess because
|
|
3518
3583
|
* something else already is. The mask is OR-ed into whatever handlers
|
|
3519
3584
|
* have already asked for.
|
|
3585
|
+
*
|
|
3586
|
+
* A mask this connection already holds is a request that would change
|
|
3587
|
+
* nothing, so it resolves without one — `eventMask` is what this
|
|
3588
|
+
* connection selected and nothing else adds to it behind the caller's
|
|
3589
|
+
* back. It resolves rather than returning undefined because the promise is
|
|
3590
|
+
* the whole interface: an awaited no-op still has to mean "you have it".
|
|
3520
3591
|
*/
|
|
3521
3592
|
selectInput(mask) {
|
|
3522
|
-
this.eventMask
|
|
3593
|
+
const added = mask & ~this.eventMask;
|
|
3594
|
+
if (added === 0) return Promise.resolve(this);
|
|
3595
|
+
this.eventMask |= added;
|
|
3523
3596
|
return new Promise((resolve, reject) => {
|
|
3524
|
-
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) =>
|
|
3525
|
-
|
|
3526
|
-
|
|
3597
|
+
this.X.ChangeWindowAttributes(this.id, { eventMask: this.eventMask }, (err) => {
|
|
3598
|
+
if (!err) return resolve(this);
|
|
3599
|
+
// a refused write left the server's mask as it was — give back the
|
|
3600
|
+
// bits, or the next call would find them held and skip a request
|
|
3601
|
+
// that has not happened yet
|
|
3602
|
+
this.eventMask &= ~added;
|
|
3603
|
+
reject(err);
|
|
3604
|
+
});
|
|
3527
3605
|
});
|
|
3528
3606
|
}
|
|
3529
3607
|
|