summaryrefslogtreecommitdiff
path: root/prober.py
blob: 4bdcc72f20a9299d3261dc404107d76824b5e1f8 (plain)
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
import subprocess
import platform
import os
import sys

# Set these to None for compile/link debugging or subprocess.PIPE to silence 
# compiler warnings and errors.
STDOUT = subprocess.PIPE
STDERR = subprocess.PIPE
# STDOUT = None
# STDERR = None

# This is the max length that I want a printed line to be.
MAX_LINE_LENGTH = 78

PY_MAJOR_VERSION = sys.version_info[0]

def line_wrap_paragraph(s):
    # Format s with terminal-friendly line wraps.
    done = False
    beginning = 0
    end = MAX_LINE_LENGTH - 1
    lines = [ ]
    while not done:
        if end >= len(s):
            done = True
            lines.append(s[beginning:])
        else:
            last_space = s[beginning:end].rfind(' ') 

            lines.append(s[beginning:beginning + last_space])
            beginning += (last_space + 1)
            end = beginning + MAX_LINE_LENGTH - 1

    return lines


def print_bad_news(value_name, default):
    s = "Setup can't determine %s on your system, so it will default to %s which may not be correct." \
            % (value_name, default)
    plea = "Please report this message and your operating system info to the package maintainer listed in the README file."
    
    lines = line_wrap_paragraph(s) + [''] + line_wrap_paragraph(plea)

    border = '*' * MAX_LINE_LENGTH
    
    s = border + "\n* " + ('\n* '.join(lines)) + '\n' + border
    
    print (s)


def does_build_succeed(filename, linker_options = ""):
    # Utility function that returns True if the file compiles and links
    # successfully, False otherwise.
    # Two things to note here --
    #   - If there's a linker option like -lrt, it needs to come *after*
    #     the specification of the C file or linking will fail on Ubuntu 11.10
    #     (maybe because of the gcc version?)
    #   - Some versions of Linux place the sem_xxx() functions in libpthread.
    #     Rather than testing whether or not it's needed, I just specify it
    #     everywhere since it's harmless to specify it when it's not needed.
    cmd = "cc -Wall -o ./prober/foo ./prober/%s %s -lpthread" % (filename, linker_options)

    p = subprocess.Popen(cmd, shell=True, stdout=STDOUT, stderr=STDERR)
        
    # p.wait() returns the process' return code, so 0 implies that 
    # the compile & link succeeded.
    return not bool(p.wait())


def compile_and_run(filename, linker_options = ""):
    # Utility function that returns the stdout output from running the 
    # compiled source file; None if the compile fails.
    cmd = "cc -Wall -o ./prober/foo %s ./prober/%s" % (linker_options, filename)

    p = subprocess.Popen(cmd, shell=True, stdout=STDOUT, stderr=STDERR)
        
    if p.wait(): 
        # uh-oh, compile failed
        return None
    else:
        s = subprocess.Popen(["./prober/foo"], 
                             stdout=subprocess.PIPE).communicate()[0]
        return s.strip().decode()
        

def get_sysctl_value(name):
    """Given a sysctl name (e.g. 'kern.mqueue.maxmsg'), returns sysctl's value
    for that variable or None if the sysctl call fails (unknown name, not 
    a BSD-ish system, etc.)

    Only makes sense on systems that implement sysctl (BSD derivatives). 
    """
    s = None
    try: 
        # I redirect stderr to /dev/null because if sysctl is availble but 
        # doesn't know about the particular item I'm querying, it will 
        # kvetch with a message like 'second level name mqueue in 
        # kern.mqueue.maxmsg is invalid'. This always happens under OS X 
        # (which doesn't have any kern.mqueue values) and under FreeBSD when
        # the mqueuefs kernel module isn't loaded.
        s = subprocess.Popen(["sysctl", "-n", name], 
                              stdout=subprocess.PIPE,
                              stderr=open(os.devnull, 'rw')).communicate()[0]
        s = s.strip().decode()
    except:
        pass
    
    return s


def sniff_realtime_lib():
    rc = None
    filename = "sniff_realtime_lib.c"

    if does_build_succeed(filename):
        # Realtime libs not needed
        rc = False
    else:
        # cc failed when not linked to realtime libs; let's try again 
        # with the realtime libs involved and see if things go better.
        if does_build_succeed(filename, "-lrt"):
            # Realtime libs are needed
            rc = True

    if rc == None:
        # Unable to determine whether or not I needed the realtime libs.
        # That's bad! Print a warning, set the return code to False
        # and hope for the best.
        rc = False
        print_bad_news("if it needs to link to the realtime libraries", "'no'")

    return rc
    

def sniff_sem_getvalue(linker_options):
    return does_build_succeed("sniff_sem_getvalue.c", linker_options)


def sniff_sem_timedwait(linker_options):
    return does_build_succeed("sniff_sem_timedwait.c", linker_options)


def sniff_sem_value_max():
    # default is to return None which means that it is #defined in a standard
    # header file and doesn't need to be added to my custom header file.
    sem_value_max = None
    
    if not does_build_succeed("sniff_sem_value_max.c"):
        # OpenSolaris 2008.05 doesn't #define SEM_VALUE_MAX. (This may
        # be true elsewhere too.) Ask sysconf() instead if it exists.
        # Note that sys.sysconf_names doesn't exist under Cygwin.
        if hasattr(os, "sysconf_names") and \
           ("SC_SEM_VALUE_MAX" in os.sysconf_names):
            sem_value_max = os.sysconf("SC_SEM_VALUE_MAX")
        else:
            # This value of last resort should be #defined everywhere. What 
            # could possibly go wrong?
            sem_value_max = "_POSIX_SEM_VALUE_MAX"

    return sem_value_max
    

def sniff_page_size():
    DEFAULT_PAGE_SIZE = 4096
    
    # Linker options don't matter here because I'm not calling any
    # functions, just getting the value of a #define.
    page_size = compile_and_run("sniff_page_size.c")
    
    if page_size is None:
        page_size = DEFAULT_PAGE_SIZE
        print_bad_news("the value of PAGE_SIZE", page_size)

    return page_size


def sniff_mq_existence(linker_options):
    return does_build_succeed("sniff_mq_existence.c", linker_options)


def sniff_mq_prio_max():
    # MQ_PRIO_MAX is #defined in limits.h on all of the systems that I 
    # checked that support message queues at all. (I checked 2 Linux boxes,
    # OpenSolaris and FreeBSD 8.0.) 
    
    # 32 = minimum allowable max priority per POSIX; systems are permitted
    # to define a larger value.
    # ref: http://www.opengroup.org/onlinepubs/009695399/basedefs/limits.h.html
    DEFAULT_PRIORITY_MAX = 32

    max_priority = None
    # OS X up to and including 10.8 doesn't support POSIX messages queues and
    # doesn't define MQ_PRIO_MAX. Maybe this aggravation will cease in 10.9?
    if does_build_succeed("sniff_mq_prio_max.c"):
        max_priority = compile_and_run("sniff_mq_prio_max.c")
    
    if max_priority:
        try:
            max_priority = int(max_priority)
        except ValueError:
            max_priority = None

    if max_priority is None:
        # Looking for a #define didn't work; ask sysconf() instead.
        # Note that sys.sysconf_names doesn't exist under Cygwin.
        if hasattr(os, "sysconf_names") and \
           ("SC_MQ_PRIO_MAX" in os.sysconf_names):
            max_priority = os.sysconf("SC_MQ_PRIO_MAX")
        else:
            max_priority = DEFAULT_PRIORITY_MAX
            print_bad_news("the value of PRIORITY_MAX", max_priority)

    # Under OS X, os.sysconf("SC_MQ_PRIO_MAX") returns -1.
    if max_priority < 0:
        max_priority = DEFAULT_PRIORITY_MAX

    # Adjust for the fact that these are 0-based values; i.e. permitted 
    # priorities range from 0 - (MQ_PRIO_MAX - 1). So why not just make
    # the #define one smaller? Because this one goes up to eleven...
    max_priority -= 1

    # priority is an unsigned int
    return str(max_priority).strip() + "U"


def sniff_mq_max_messages():
    # This value is not defined by POSIX.
    
    # On most systems I've tested, msg Qs are implemented via mmap-ed files 
    # or a similar interface, so the only theoretical limits are imposed by the
    # file system. In practice, Linux and *BSD impose some fairly tight 
    # limits. 

    # On Linux it's available in a /proc file and often defaults to the wimpy
    # value of 10.
    
    # On FreeBSD (and other BSDs, I assume), it's available via sysctl as
    # kern.mqueue.maxmsg. On my FreeBSD 9.1 test system, it defaults to 100.

    # mqueue.h defines mq_attr.mq_maxmsg as a C long, so that's 
    # a practical limit for this value.
    
    # ref: http://linux.die.net/man/7/mq_overview
    # ref: http://www.freebsd.org/cgi/man.cgi?query=mqueuefs&sektion=5&manpath=FreeBSD+7.0-RELEASE
    # http://fxr.watson.org/fxr/source/kern/uipc_mqueue.c?v=FREEBSD91#L195
    # ref: http://groups.google.com/group/comp.unix.solaris/browse_thread/thread/aa223fc7c91f8c38
    # ref: http://cygwin.com/cgi-bin/cvsweb.cgi/src/winsup/cygwin/posix_ipc.cc?cvsroot=src
    # ref: http://cygwin.com/cgi-bin/cvsweb.cgi/src/winsup/cygwin/include/mqueue.h?cvsroot=src
    mq_max_messages = None

    # Try to get the value from where Linux stores it.
    try:
        mq_max_messages = int(open("/proc/sys/fs/mqueue/msg_max").read())
    except:
        # Oh well.
        pass

    if not mq_max_messages:
        # Maybe we're on BSD.
        mq_max_messages = get_sysctl_value('kern.mqueue.maxmsg')
        if mq_max_messages:
            mq_max_messages = int(mq_max_messages)

    if not mq_max_messages:
        # We're on a non-Linux, non-BSD system, or OS X, or BSD with 
        # the mqueuefs kernel module not loaded (which it's not, by default,
        # under FreeBSD 8.x and 9.x. which are the only systems I've tested). 
        #
        # If we're on FreeBSD and mqueuefs isn't loaded when this code runs,
        # sysctl won't be able to provide mq_max_messages to me. (I assume other
        # BSDs behave the same.) If I use too large of a default, then every
        # attempt to create a message queue via posix_ipc will fail with
        # "ValueError: Invalid parameter(s)"  unless the user explicitly sets
        # the max_messages param.
        if platform.system().endswith("BSD"):
            # 100 is the value I see under FreeBSD 9.2. I hope this works
            # elsewhere!
            mq_max_messages = 100
        else:
            # We're on a non-Linux, non-BSD system. I take a wild guess at an
            # appropriate value. The max possible is > 2 billion, but the 
            # values used by Linux and FreeBSD suggest that a smaller default
            # is wiser.
            mq_max_messages = 1024

    return mq_max_messages


def sniff_mq_max_message_size_default():
    # The max message size is not defined by POSIX.
    
    # On most systems I've tested, msg Qs are implemented via mmap-ed files 
    # or a similar interface, so the only theoretical limits are imposed by 
    # the file system. In practice, Linux and *BSD impose some tighter limits. 

    # On Linux, max message size is available in a /proc file and often 
    # defaults to the value of 8192.

    # On FreeBSD (and other BSDs, I assume), it's available via sysctl as
    # kern.mqueue.maxmsgsize. On my FreeBSD 9.1 test system, it defaults to 
    # 16384. 

    # mqueue.h defines mq_attr.mq_msgsize as a C long, so that's 
    # a practical limit for this value.
    
    # Further complicating things is the fact that the module has to allocate
    # a buffer the size of the queue's max message every time receive() is 
    # called, so it would be a bad idea to set this default to the max.
    # I set it to 8192 -- not too small, not too big. I only set it smaller
    # if I'm on a system that tells me I must do so.
    DEFAULT = 8192 
    mq_max_message_size_default = 0

    # Try to get the value from where Linux stores it.
    try:
        mq_max_message_size_default = \
                            int(open("/proc/sys/fs/mqueue/msgsize_max").read())
    except:
        # oh well
        pass

    if not mq_max_message_size_default:
        # Maybe we're on BSD.
        mq_max_message_size_default = get_sysctl_value('kern.mqueue.maxmsgsize')
        if mq_max_message_size_default:
            mq_max_message_size_default = int(mq_max_message_size_default)

    if not mq_max_message_size_default:
        mq_max_message_size_default = DEFAULT
        
    return mq_max_message_size_default



def probe():
    linker_options = ""
    d = { }
    
    f = open("VERSION")
    d["POSIX_IPC_VERSION"] = '"%s"' % f.read().strip()
    f.close()

    # Sniffing of the realtime libs has to go early in the list so as 
    # to provide correct linker options to the rest of the tests.     
    if "Darwin" in platform.uname():
        # I skip the test under Darwin/OS X for two reasons. First, I know
        # it isn't needed there. Second, I can't even compile the test for
        # the realtime lib because it references mq_unlink() which OS X
        # doesn't support. Unfortunately sniff_realtime_lib.c *must* 
        # reference mq_unlink() or some other mq_xxx() function because 
        # it is only the message queues that need the realtime libs under
        # FreeBSD. 
        realtime_lib_is_needed = False
    else:
        # Some platforms (e.g. Linux & OpenSuse) require linking to librt
        realtime_lib_is_needed = sniff_realtime_lib()

    if realtime_lib_is_needed:
        d["REALTIME_LIB_IS_NEEDED"] = ""
        linker_options = " -lrt "

    d["PAGE_SIZE"] = sniff_page_size()
    
    if sniff_sem_getvalue(linker_options):
        d["SEM_GETVALUE_EXISTS"] = ""

    if ("SEM_GETVALUE_EXISTS" in d) and ("Darwin" in platform.uname()):
        # sem_getvalue() isn't available on OS X. The function exists but 
        # always returns -1 (under OS X 10.9) or ENOSYS ("Function not 
        # implemented") under some earlier version(s).
        del d["SEM_GETVALUE_EXISTS"]

    if sniff_sem_timedwait(linker_options):
        d["SEM_TIMEDWAIT_EXISTS"] = ""
        
    d["SEM_VALUE_MAX"] = sniff_sem_value_max()
    # A return of None means that I don't need to #define this myself.
    if d["SEM_VALUE_MAX"] is None:
        del d["SEM_VALUE_MAX"]

    if sniff_mq_existence(linker_options):
        d["MESSAGE_QUEUE_SUPPORT_EXISTS"] = ""
        
    d["QUEUE_MESSAGES_MAX_DEFAULT"] = sniff_mq_max_messages()
    d["QUEUE_MESSAGE_SIZE_MAX_DEFAULT"] = sniff_mq_max_message_size_default()
    d["QUEUE_PRIORITY_MAX"] = sniff_mq_prio_max()

    if PY_MAJOR_VERSION == 2:
        # I only need this for Python 2.x
        d["PY_INT_MAX"] = sys.maxint
        

    msg = """/*
This header file was generated when you ran setup. Once created, the setup
process won't overwrite it, so you can adjust the values by hand and 
recompile if you need to.

On your platform, this file may contain only this comment -- that's OK!

To recreate this file, just delete it and re-run setup.py.
*/

"""
    filename = "probe_results.h"    
    if not os.path.exists(filename):
        lines = ["#define %s\t\t%s" % (key, d[key]) for key in d if key != "PAGE_SIZE"]

        # PAGE_SIZE gets some special treatment. It's defined in header files 
        # on some systems in which case I might get a redefinition error in 
        # my header file, so I wrap it in #ifndef/#endif.
        
        lines.append("#ifndef PAGE_SIZE")
        lines.append("#define PAGE_SIZE\t\t%s" % d["PAGE_SIZE"])
        lines.append("#endif")
        
        # A trailing '\n' keeps compilers happy...
        open(filename, "w").write(msg + '\n'.join(lines) + '\n')
        
    return d


if __name__ == "__main__":
    print (probe())