File size: 5,654 Bytes
d82cf6a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""Fork a child process and inform it of mode changes to each screen.  The
child waits until the parent process dies, and then connects to each X server 
with a mode change and restores the mode.

This emulates the behaviour of Windows and Mac, so that resolution changes
made by an application are not permanent after the program exits, even if
the process is terminated uncleanly.

The child process is communicated to via a pipe, and watches for parent
death with a Linux extension signal handler.
"""

import ctypes
import os
import signal
import struct
import threading

from pyglet.libs.x11 import xlib
from pyglet.util import asbytes

try:
    from pyglet.libs.x11 import xf86vmode
except:
    # No xf86vmode... should not be switching modes.
    pass

_restore_mode_child_installed = False
_restorable_screens = set()
_mode_write_pipe = None


# Mode packets tell the child process how to restore a given display and
# screen.  Only one packet should be sent per display/screen (more would
# indicate redundancy or incorrect restoration).  Packet format is:
#   display (max 256 chars), 
#   screen
#   width
#   height
#   rate
class ModePacket:
    format = '256siHHI'
    size = struct.calcsize(format)

    def __init__(self, display, screen, width, height, rate):
        self.display = display
        self.screen = screen
        self.width = width
        self.height = height
        self.rate = rate

    def encode(self):
        return struct.pack(self.format, self.display, self.screen, self.width, self.height, self.rate)

    @classmethod
    def decode(cls, data):
        display, screen, width, height, rate = struct.unpack(cls.format, data)
        return cls(display.strip(asbytes('\0')), screen, width, height, rate)

    def __repr__(self):
        return f'{self.__class__.__name__}({self.display}, ' \
               f'{self.screen}, {self.width}, {self.height}, {self.rate})'

    def set(self):
        display = xlib.XOpenDisplay(self.display)
        modes, n_modes = get_modes_array(display, self.screen)
        mode = get_matching_mode(modes, n_modes, self.width, self.height, self.rate)
        if mode is not None:
            xf86vmode.XF86VidModeSwitchToMode(display, self.screen, mode)
        free_modes_array(modes, n_modes)
        xlib.XCloseDisplay(display)


def get_modes_array(display, screen):
    count = ctypes.c_int()
    modes = ctypes.POINTER(ctypes.POINTER(xf86vmode.XF86VidModeModeInfo))()
    xf86vmode.XF86VidModeGetAllModeLines(display, screen, count, modes)
    return modes, count.value


def get_matching_mode(modes, n_modes, width, height, rate):
    # Copy modes out of list and free list
    for i in range(n_modes):
        mode = modes.contents[i]
        if (mode.hdisplay == width and
                mode.vdisplay == height and
                mode.dotclock == rate):
            return mode
    return None


def free_modes_array(modes, n_modes):
    for i in range(n_modes):
        mode = modes.contents[i]
        if mode.privsize:
            xlib.XFree(mode.private)
    xlib.XFree(modes)


def _install_restore_mode_child():
    global _mode_write_pipe
    global _restore_mode_child_installed

    if _restore_mode_child_installed:
        return

    # Parent communicates to child by sending "mode packets" through a pipe:
    mode_read_pipe, _mode_write_pipe = os.pipe()

    if os.fork() == 0:
        # Child process (watches for parent to die then restores video mode(s).
        os.close(_mode_write_pipe)

        # Set up SIGHUP to be the signal for when the parent dies.
        PR_SET_PDEATHSIG = 1
        libc = ctypes.cdll.LoadLibrary('libc.so.6')
        libc.prctl.argtypes = (ctypes.c_int, ctypes.c_ulong, ctypes.c_ulong,
                               ctypes.c_ulong, ctypes.c_ulong)
        libc.prctl(PR_SET_PDEATHSIG, signal.SIGHUP, 0, 0, 0)

        # SIGHUP indicates the parent has died.  The child lock is unlocked, it
        # stops reading from the mode packet pipe and restores video modes on
        # all displays/screens it knows about.
        def _sighup(signum, frame):
            parent_wait_lock.release()

        parent_wait_lock = threading.Lock()
        parent_wait_lock.acquire()
        signal.signal(signal.SIGHUP, _sighup)

        # Wait for parent to die and read packets from parent pipe
        packets = []
        buffer = asbytes('')
        while parent_wait_lock.locked():
            try:
                data = os.read(mode_read_pipe, ModePacket.size)
                buffer += data
                # Decode packets
                while len(buffer) >= ModePacket.size:
                    packet = ModePacket.decode(buffer[:ModePacket.size])
                    packets.append(packet)
                    buffer = buffer[ModePacket.size:]
            except OSError:
                pass  # Interrupted system call

        for packet in packets:
            packet.set()
        os._exit(0)

    else:
        # Parent process.  Clean up pipe then continue running program as
        # normal.  Send mode packets through pipe as additional
        # displays/screens are mode switched.
        os.close(mode_read_pipe)
        _restore_mode_child_installed = True


def set_initial_mode(mode):
    _install_restore_mode_child()

    display = xlib.XDisplayString(mode.screen.display._display)
    screen = mode.screen.display.x_screen

    # Only store one mode per screen.
    if (display, screen) in _restorable_screens:
        return

    packet = ModePacket(display, screen, mode.width, mode.height, mode.rate)

    os.write(_mode_write_pipe, packet.encode())
    _restorable_screens.add((display, screen))