platform.py 13.5 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python
#
# platform.py - Platform information
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`Platform` class, which is a container
of information about the current platform we are running on. A single
``Platform`` instance is created when this module is first imported, and
is available as a module attribute called :attr:`platform`.
"""


import logging

import os
17
import os.path as op
Paul McCarthy's avatar
Paul McCarthy committed
18
import sys
19
import importlib
20

21
22
import fsl.utils.notifier   as notifier
import fsl.utils.deprecated as deprecated
23

24
25
26
27
28
29
30
31
32
33
34
# An annoying consequence of using
# a system-module name for our own
# module is that we can't import
# it directly (as it will attempt
# to import itself, i.e. this module).
#
# This is only necessary in Python 2.x
# (as python 3 disallows relative
# imports).
builtin_platform = importlib.import_module('platform')

35
36
37
38

log = logging.getLogger(__name__)


39
40
41
WX_UNKNOWN = 0
"""Identifier for the :attr:`Platform.wxFlavour` and
:attr:`Platform.wxPlatform` properties indicating an unknown/undetermined
Paul McCarthy's avatar
Paul McCarthy committed
42
flavour/platform.
43
44
45
"""


46
47
48
49
50
51
52
WX_PYTHON  = 1
"""Identifier for the :attr:`Platform.wxFlavour` property, indicating that
we are running standard wx Python.
"""


WX_PHOENIX = 2
53
"""Identifier for the :attr:`Platform.wxFlavour` property, indicating that we
54
55
56
57
58
are running wx Python/Phoenix.
"""


WX_MAC_COCOA = 1
Paul McCarthy's avatar
Paul McCarthy committed
59
"""Identifier for the :attr:`Platform.wxPlatform` property, indicating that we
60
61
62
63
64
are running the OSX cocoa wx build.
"""


WX_MAC_CARBON = 2
Paul McCarthy's avatar
Paul McCarthy committed
65
"""Identifier for the :attr:`Platform.wxPlatform` property, indicating that we
66
67
68
69
70
are running the OSX carbon wx build.
"""


WX_GTK = 3
Paul McCarthy's avatar
Paul McCarthy committed
71
"""Identifier for the :attr:`Platform.wxPlatform` property, indicating that we
72
73
74
75
76
77
78
79
are running the Linux/GTK wx build.
"""


class Platform(notifier.Notifier):
    """The ``Platform`` class contains a handful of properties which contain
    information about the platform we are running on.

80
81
    .. note:: The values of the :attr:`glVersion` and :attr:`glRenderer`
              properties are not automatically set - they will only contain
82
83
              a value if one is assigned to them. *FSLeyes* does this during
              startup, in the :func:`fsleyes.gl.bootstrap` function.
84

85
86
    .. autosummary::

Paul McCarthy's avatar
Paul McCarthy committed
87
88
       os
       frozen
89
       fsldir
90
       fsldevdir
91
       haveGui
92
       canHaveGui
93
       inSSHSession
94
       wxPlatform
95
       wxFlavour
96
97
       glVersion
       glRenderer
98
       glIsSoftwareRenderer
99
100
    """

101

102
103
    def __init__(self):
        """Create a ``Platform`` instance. """
104
105
106

        # For things which 'from fsl.utils.platform import platform',
        # these identifiers are available on the platform instance
107
        self.WX_UNKNOWN    = WX_UNKNOWN
108
109
110
111
112
        self.WX_PYTHON     = WX_PYTHON
        self.WX_PHOENIX    = WX_PHOENIX
        self.WX_MAC_COCOA  = WX_MAC_COCOA
        self.WX_MAC_CARBON = WX_MAC_CARBON
        self.WX_GTK        = WX_GTK
113

114
115
116
117
        # initialise fsldir - see fsldir.setter
        self.fsldir = self.fsldir

        # These are all initialised on first access
118
119
        self.__glVersion    = None
        self.__glRenderer   = None
120
        self.__glIsSoftware = None
121
        self.__fslVersion   = None
122
        self.__canHaveGui   = None
123

124
125
126
127
128
        # If one of the SSH_/VNC environment
        # variables is set, then we're probably
        # running over SSH/VNC.
        sshVars = ['SSH_CLIENT', 'SSH_TTY']
        vncVars = ['VNCDESKTOP', 'X2GO_SESSION', 'NXSESSIONID']
129

130
131
        self.__inSSHSession = any(s in os.environ for s in sshVars)
        self.__inVNCSession = any(v in os.environ for v in vncVars)
132

133

Paul McCarthy's avatar
Paul McCarthy committed
134
135
136
137
138
139
140
    @property
    def os(self):
        """The operating system name. Whatever is returned by the built-in
        ``platform.system`` function.
        """
        return builtin_platform.system()

141

Paul McCarthy's avatar
Paul McCarthy committed
142
    @property
143
144
145
146
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
Paul McCarthy's avatar
Paul McCarthy committed
147
148
149
150
151
152
    def frozen(self):
        """``True`` if we are running in a compiled/frozen application,
        ``False`` otherwise.
        """
        return getattr(sys, 'frozen', False)

153
154

    @property
155
156
157
158
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
159
    def haveGui(self):
160
161
162
163
164
165
166
167
        """``True`` if we are running with a GUI, ``False`` otherwise.

        This currently equates to testing whether a display is available
        (see :meth:`canHaveGui`) and whether a ``wx.App`` exists. It
        previously also tested whether an event loop was running, but this
        is not compatible with execution from IPython/Jupyter notebook, where
        the event loop is called periodically, and so is not always running.
        """
168
        try:
169
            import wx  # pylint: disable=import-outside-toplevel
170
            app = wx.GetApp()
171
172
173
174
175
176

            # TODO Previously this conditional
            #      also used app.IsMainLoopRunning()
            #      to check that the wx main loop
            #      was running. But this doesn't
            #      suit situations where a non-main
177
178
179
            #      event loop is running, or where
            #      the mainloop is periodically
            #      started and stopped (e.g. when
180
181
182
183
184
185
            #      the event loop is being run by
            #      IPython).
            #
            #      In c++ wx, there is the
            #      wx.App.UsesEventLoop method, but
            #      this is not presently exposed to
186
187
188
            #      Python code (and wouldn't help
            #      to detect the loop start/stop
            #      scenario).
189
190
191
            #
            #      So this constraint has been
            #      (hopefully) temporarily relaxed
192
193
            #      until I can think of a better
            #      solution.
194
            return (self.canHaveGui and
195
                    app is not None)
196

197
198
        except ImportError:
            return False
199

200

201
    @property
202
203
204
205
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
206
207
    def canHaveGui(self):
        """``True`` if it is possible to create a GUI, ``False`` otherwise. """
208
209
210
211
212
213
214
215
216
217
218

        # Determine if a display is available. Note that
        # calling the IsDisplayAvailable function will
        # cause the application to steal focus under OSX!
        if self.__canHaveGui is None:
            try:
                import wx  # pylint: disable=import-outside-toplevel
                self.__canHaveGui = wx.App.IsDisplayAvailable()
            except ImportError:
                self.__canHaveGui = False

219
        return self.__canHaveGui
220
221


222
    @property
223
224
225
226
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
227
228
229
230
231
232
    def inSSHSession(self):
        """``True`` if this application is running over an SSH session,
        ``False`` otherwise.
        """
        return self.__inSSHSession

233

234
    @property
235
236
237
238
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
239
240
241
242
243
244
245
246
247
248
249
250
    def inVNCSession(self):
        """``True`` if this application is running over a VNC (or similar)
        session, ``False`` otherwise. Currently, the following remote desktop
        environments are detected:

          - VNC
          - x2go
          - NoMachine
        """
        return self.__inVNCSession


251
    @property
252
253
254
255
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
256
    def wxPlatform(self):
257
258
        """One of :data:`WX_UNKNOWN`, :data:`WX_MAC_COCOA`,
        :data:`WX_MAC_CARBON`, or :data:`WX_GTK`, indicating the wx platform.
259
        """
260

261
        if not self.canHaveGui:
262
263
            return WX_UNKNOWN

264
        import wx  # pylint: disable=import-outside-toplevel
265
266
267

        pi = [t.lower() for t in wx.PlatformInfo]

268
269
270
271
        if   any('cocoa'  in p for p in pi): plat = WX_MAC_COCOA
        elif any('carbon' in p for p in pi): plat = WX_MAC_CARBON
        elif any('gtk'    in p for p in pi): plat = WX_GTK
        else:                                plat = WX_UNKNOWN
272

273
        if plat is WX_UNKNOWN:
Paul McCarthy's avatar
Paul McCarthy committed
274
275
            log.warning('Could not determine wx platform from '
                        'information: {}'.format(pi))
276

Paul McCarthy's avatar
Paul McCarthy committed
277
        return plat
278

279

280
    @property
281
282
283
284
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
285
    def wxFlavour(self):
286
287
        """One of :data:`WX_UNKNOWN`, :data:`WX_PYTHON` or :data:`WX_PHOENIX`,
        indicating the wx flavour.
288
        """
289

290
        if not self.canHaveGui:
291
292
            return WX_UNKNOWN

293
        import wx  # pylint: disable=import-outside-toplevel
294
295
296
297
298

        pi        = [t.lower() for t in wx.PlatformInfo]
        isPhoenix = False

        for tag in pi:
299
            if 'phoenix' in tag:
300
301
302
303
304
                isPhoenix = True
                break

        if isPhoenix: return WX_PHOENIX
        else:         return WX_PYTHON
305
306
307
308
309
310
311
312
313
314


    @property
    def fsldir(self):
        """The FSL installation location.

        .. note:: The ``fsldir`` property can be updated - when it is changed,
                  any registered listeners are notified via the
                  :class:`.Notifier` interface.
        """
315
316
        return os.environ.get('FSLDIR', None)

317

318
319
320
321
    @property
    def fsldevdir(self):
        """The FSL development directory location. """
        return os.environ.get('FSLDEVDIR', None)
322

323

324
325
    @property
    def fslwsl(self):
326
327
328
        """Boolean flag indicating whether FSL is installed in Windows
        Subsystem for Linux
        """
329
        return self.fsldir is not None and self.fsldir.startswith("\\\\wsl$")
330

331

332
333
334
335
336
    @fsldir.setter
    def fsldir(self, value):
        """Changes the value of the :attr:`fsldir` property, and notifies any
        registered listeners.
        """
337
338
339
340
341
342

        if value is not None:
            value = value.strip()

        if   value is None:        pass
        elif value == '':          value = None
343
344
        elif not op.exists(value): value = None
        elif not op.isdir(value):  value = None
345

346
347
348
        if value is None:
            os.environ.pop('FSLDIR', None)
        else:
349
            os.environ['FSLDIR'] = value
350

Paul McCarthy's avatar
Paul McCarthy committed
351
352
            # Set the FSL version field if we can
            versionFile = op.join(value, 'etc', 'fslversion')
353

Paul McCarthy's avatar
Paul McCarthy committed
354
355
            if op.exists(versionFile):
                with open(versionFile, 'rt') as f:
356
                    # split string at colon for new hash style versions
357
358
359
                    # first object in list is the non-hashed version string
                    # (e.g. 6.0.2) if no ":hash:" then standard FSL version
                    # string is still returned
360
                    self.__fslVersion = f.read().strip().split(":")[0]
361
362

        self.notify(value=value)
363

364

365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
    @fsldevdir.setter
    def fsldevdir(self, value):
        """Changes the value of the :attr:`fsldevdir` property, and notifies
        any registered listeners.
        """

        if value is not None:
            value = value.strip()

        if   value is None:        pass
        elif value == '':          value = None
        elif not op.exists(value): value = None
        elif not op.isdir(value):  value = None

        if value is None:
            os.environ.pop('FSLDEVDIR', None)
        else:
            os.environ['FSLDEVDIR'] = value


385
386
387
388
389
390
391
    @property
    def fslVersion(self):
        """Returns the FSL version as a string, e.g. ``'5.0.9'``. Returns
        ``None`` if a FSL installation could not be found.
        """
        return self.__fslVersion

392

393
    @property
394
395
396
397
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
398
399
400
401
402
403
    def glVersion(self):
        """Returns the available OpenGL version, or ``None`` if it has not
        been set.
        """
        return self.__glVersion

404

405
    @glVersion.setter
406
407
408
409
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
410
411
412
413
414
415
    def glVersion(self, value):
        """Set the available OpenGL version. """
        self.__glVersion = value


    @property
416
417
418
419
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
420
421
422
423
424
425
426
427
    def glRenderer(self):
        """Returns the available OpenGL renderer, or ``None`` if it has not
        been set.
        """
        return self.__glRenderer


    @glRenderer.setter
428
429
430
431
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
432
433
434
435
    def glRenderer(self, value):
        """Set the available OpenGL renderer. """
        self.__glRenderer = value

436
437
438
439
440
441
442
443
444
445
446
447
448
        value = value.lower()

        # There doesn't seem to be any quantitative
        # method for determining whether we are using
        # software-based rendering, so a hack is
        # necessary.
        self.__glIsSoftware = any((
            'software' in value,
            'chromium' in value,
        ))


    @property
449
450
451
452
    @deprecated.deprecated(
        '3.6.0',
        '4.0.0',
        'Equivalent functionality is available in fsleyes-widgets.')
453
454
455
456
457
458
459
460
461
    def glIsSoftwareRenderer(self):
        """Returns ``True`` if the OpenGL renderer is software based,
        ``False`` otherwise, or ``None`` if the renderer has not yet been set.

        .. note:: This check is based on heuristics, ans is not guaranteed to
                  be correct.
        """
        return self.__glIsSoftware

462

463
464
465
466
467
platform = Platform()
"""An instance of the :class:`Platform` class. Feel free to create your own
instance, but be aware that if you do so you will not be updated of changes
to the :attr:`Platform.fsldir` property.
"""