File size: 14,504 Bytes
72268ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# Copyright (c) 2008-2011 by Enthought, Inc.
# Copyright (c) 2013-2017 Continuum Analytics, Inc.
# All rights reserved.

from __future__ import absolute_import, unicode_literals

import ctypes
import logging
import os
from os.path import isdir, join, exists, split
import sys
import locale


from .utils import rm_empty_dir, rm_rf
from .knownfolders import get_folder_path, FOLDERID
# KNOWNFOLDERID does provide a direct path to Quick Launch.  No additional path necessary.
from .winshortcut import create_shortcut


# This allows debugging installer issues using DebugView from Microsoft.
OutputDebugString = ctypes.windll.kernel32.OutputDebugStringW
OutputDebugString.argtypes = [ctypes.c_wchar_p]


class DbgViewHandler(logging.Handler):
    def emit(self, record):
        OutputDebugString(self.format(record))


logger = logging.getLogger("menuinst_win32")
logger.setLevel(logging.DEBUG)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARNING)
dbgview = DbgViewHandler()
dbgview.setLevel(logging.DEBUG)
logger.addHandler(dbgview)
logger.addHandler(stream_handler)

# When running as 'nt authority/system' as sometimes people do via SCCM,
# various folders do not exist, such as QuickLaunch. This doesn't matter
# as we'll use the "system" key finally and check for the "quicklaunch"
# subkey before adding any Quick Launch menu items.

# It can happen that some of the dirs[] entires refer to folders that do not
# exist, in which case, the 2nd entry of the value tuple is a sub-class of
# Exception.

dirs_src = {"system": {  "desktop": get_folder_path(FOLDERID.PublicDesktop),
                           "start": get_folder_path(FOLDERID.CommonPrograms),
                       "documents": get_folder_path(FOLDERID.PublicDocuments),
                         "profile": get_folder_path(FOLDERID.Profile)},

            "user": {    "desktop": get_folder_path(FOLDERID.Desktop),
                           "start": get_folder_path(FOLDERID.Programs),
                     "quicklaunch": get_folder_path(FOLDERID.QuickLaunch),
                       "documents": get_folder_path(FOLDERID.Documents),
                         "profile": get_folder_path(FOLDERID.Profile)}}


def folder_path(preferred_mode, check_other_mode, key):
    ''' This function implements all heuristics and workarounds for messed up
        KNOWNFOLDERID registry values. It's also verbose (OutputDebugStringW)
        about whether fallbacks worked or whether they would have worked if
        check_other_mode had been allowed.
    '''
    other_mode = 'system' if preferred_mode == 'user' else 'user'
    path, exception = dirs_src[preferred_mode][key]
    if not exception:
        return path
    logger.info("WARNING: menuinst key: '%s'\n"
                "                 path: '%s'\n"
                "     .. excepted with: '%s' in knownfolders.py, implementing workarounds .."
                % (key, path, type(exception).__name__))
    # Since I have seen 'user', 'documents' set as '\\vmware-host\Shared Folders\Documents'
    # when there's no such server, we check 'user', 'profile' + '\Documents' before maybe
    # trying the other_mode (though I have chickened out on that idea).
    if preferred_mode == 'user' and key == 'documents':
        user_profile, exception = dirs_src['user']['profile']
        if not exception:
            path = join(user_profile, 'Documents')
            if os.access(path, os.W_OK):
                logger.info("  .. worked-around to: '%s'" % (path))
                return path
    path, exception = dirs_src[other_mode][key]
    # Do not fall back to something we cannot write to.
    if exception:
        if check_other_mode:
            logger.info("     .. despite 'check_other_mode'\n"
                        "        and 'other_mode' 'path' of '%s'\n"
                        "        it excepted with: '%s' in knownfolders.py" % (path,
                                                                    type(exception).__name__))
        else:
            logger.info("     .. 'check_other_mode' is False,\n"
                        "        and 'other_mode' 'path' is '%s'\n"
                        "        but it excepted anyway with: '%s' in knownfolders.py" % (path, type(exception).__name__))
        return None
    if not check_other_mode:
        logger.info("     .. due to lack of 'check_other_mode' not picking\n"
                    "        non-excepting path of '%s'\n in knownfolders.py" % (path))
        return None
    return path


def quoted(s):
    """
    quotes a string if necessary.
    """
    # strip any existing quotes
    s = s.strip(u'"')
    # don't add quotes for minus or leading space
    if s[0] in (u'-', u' '):
        return s
    if u' ' in s or u'/' in s:
        return u'"%s"' % s
    else:
        return s


def ensure_pad(name, pad="_"):
    """

    Examples:
        >>> ensure_pad('conda')
        '_conda_'

    """
    if not name or name[0] == name[-1] == pad:
        return name
    else:
        return "%s%s%s" % (pad, name, pad)


def to_unicode(var, codec=locale.getpreferredencoding()):
    if sys.version_info[0] < 3 and isinstance(var, unicode):
        return var
    if not codec:
        codec = "utf-8"
    if hasattr(var, "decode"):
        var = var.decode(codec)
    return var


def to_bytes(var, codec=locale.getpreferredencoding()):
    if isinstance(var, bytes):
        return var
    if not codec:
        codec="utf-8"
    if hasattr(var, "encode"):
        var = var.encode(codec)
    return var


unicode_root_prefix = to_unicode(sys.prefix)
if u'\\envs\\' in unicode_root_prefix:
    logger.warn('menuinst called from non-root env %s', unicode_root_prefix)


def substitute_env_variables(text, dir):
    # When conda is using Menuinst, only the root conda installation ever
    # calls menuinst.  Thus, these calls to sys refer to the root conda
    # installation, NOT the child environment
    py_major_ver = sys.version_info[0]
    py_bitness = 8 * tuple.__itemsize__

    env_prefix = to_unicode(dir['prefix'])
    root_prefix = to_unicode(dir['root_prefix'])
    text = to_unicode(text)
    env_name = to_unicode(dir['env_name'])

    for a, b in (
        (u'${PREFIX}', env_prefix),
        (u'${ROOT_PREFIX}', root_prefix),
        (u'${DISTRIBUTION_NAME}', os.path.split(root_prefix)[-1].capitalize()),
        (u'${PYTHON_SCRIPTS}',
          os.path.normpath(join(env_prefix, u'Scripts')).replace(u"\\", u"/")),
        (u'${MENU_DIR}', join(env_prefix, u'Menu')),
        (u'${PERSONALDIR}', dir['documents']),
        (u'${USERPROFILE}', dir['profile']),
        (u'${ENV_NAME}', env_name),
        (u'${PY_VER}', u'%d' % (py_major_ver)),
        (u'${PLATFORM}', u"(%s-bit)" % py_bitness),
        ):
        if b:
            text = text.replace(a, b)
    return text


class Menu(object):
    def __init__(self, name, prefix=unicode_root_prefix, env_name=u"", mode=None, root_prefix=unicode_root_prefix):
        """
        Prefix is the system prefix to be used -- this is needed since
        there is the possibility of a different Python's packages being managed.
        """

        # bytestrings passed in need to become unicode
        self.prefix = to_unicode(prefix)
        self.root_prefix = to_unicode(root_prefix)
        used_mode = mode if mode else ('user' if exists(join(self.prefix, u'.nonadmin')) else 'system')
        logger.debug("Menu: name: '%s', prefix: '%s', env_name: '%s', mode: '%s', used_mode: '%s', root_prefix: '%s'"
                    % (name, self.prefix, env_name, mode, used_mode, root_prefix))
        try:
            self.set_dir(name, self.prefix, env_name, used_mode, root_prefix)
        except WindowsError:
            # We get here if we aren't elevated.  This is different from
            #   permissions: a user can have permission, but elevation is still
            #   required.  If the process isn't elevated, we get the
            #   WindowsError
            if 'user' in dirs_src and used_mode == 'system':
                logger.warn("Insufficient permissions to write menu folder.  "
                            "Falling back to user location")
                try:
                    self.set_dir(name, self.prefix, env_name, 'user')
                except:
                    pass
            else:
                logger.fatal("Unable to create AllUsers menu folder")

    def set_dir(self, name, prefix, env_name, mode, root_prefix):
        self.mode = mode
        self.dir = dict()
        # I have chickened out on allowing check_other_mode. Really there needs
        # to be 3 distinct cases that 'menuinst' cares about:
        # priv-user doing system install
        # priv-user doing user-only install
        # non-priv-user doing user-only install
        # (priv-user only exists in an AllUsers installation).
        check_other_mode = False
        for k, v in dirs_src[mode].items():
            # We may want to cache self.dir to some files, one for AllUsers
            # (system) installs and one for each subsequent user install?
            self.dir[k] = folder_path(mode, check_other_mode, k)
        self.dir['prefix'] = prefix
        self.dir['root_prefix'] = root_prefix
        self.dir['env_name'] = env_name
        folder_name = substitute_env_variables(name, self.dir)
        self.path = join(self.dir["start"], folder_name)
        self.create()

    def create(self):
        if not isdir(self.path):
            os.mkdir(self.path)

    def remove(self):
        rm_empty_dir(self.path)


def extend_script_args(args, shortcut):
    try:
        args.append(shortcut['scriptargument'])
    except KeyError:
        pass
    try:
        args.extend(shortcut['scriptarguments'])
    except KeyError:
        pass


def quote_args(args):
    # cmd.exe /K or /C expects a single string argument and requires
    # doubled-up quotes when any sub-arguments have spaces:
    # https://stackoverflow.com/a/6378038/3257826
    if (len(args) > 2 and ("CMD.EXE" in args[0].upper() or "%COMSPEC%" in args[0].upper())
            and (args[1].upper() == '/K' or args[1].upper() == '/C')
            and any(' ' in arg for arg in args[2:])
    ):
        args = [
            ensure_pad(args[0], '"'),  # cmd.exe
            args[1],  # /K or /C
            '"%s"' % (' '.join(ensure_pad(arg, '"') for arg in args[2:])),  # double-quoted
        ]
    else:
        args = [quoted(arg) for arg in args]
    return args


class ShortCut(object):
    def __init__(self, menu, shortcut):
        self.menu = menu
        self.shortcut = shortcut

    def remove(self):
        self.create(remove=True)

    def create(self, remove=False):
        # Substitute env variables early because we may need to escape spaces in the value.
        args = []
        fix_win_slashes = [0]
        prefix = self.menu.prefix.replace('/', '\\')
        unicode_root_prefix = self.menu.root_prefix.replace('/', '\\')
        root_py  = join(unicode_root_prefix, u"python.exe")
        root_pyw = join(unicode_root_prefix, u"pythonw.exe")
        env_py  = join(prefix, u"python.exe")
        env_pyw = join(prefix, u"pythonw.exe")
        cwp_py  = [root_py,  join(unicode_root_prefix, u'cwp.py'), prefix, env_py]
        cwp_pyw = [root_pyw, join(unicode_root_prefix, u'cwp.py'), prefix, env_pyw]
        if "pywscript" in self.shortcut:
            args = cwp_pyw
            fix_win_slashes = [len(args)]
            args += self.shortcut["pywscript"].split()
        elif "pyscript" in self.shortcut:
            args = cwp_py
            fix_win_slashes = [len(args)]
            args += self.shortcut["pyscript"].split()
        elif "webbrowser" in self.shortcut:
            args = [root_pyw, '-m', 'webbrowser', '-t', self.shortcut['webbrowser']]
        elif "script" in self.shortcut:
            # It is unclear whether running through cwp.py is what we want here. In
            # the long term I would rather this was made an explicit choice.
            args = [root_py,  join(unicode_root_prefix, u'cwp.py'), prefix]
            fix_win_slashes = [len(args)]
            args += self.shortcut["script"].split()
            extend_script_args(args, self.shortcut)
        elif "system" in self.shortcut:
            args = self.shortcut["system"].split()
            extend_script_args(args, self.shortcut)
        else:
            raise Exception("Nothing to do: %r" % self.shortcut)
        args = [substitute_env_variables(arg, self.menu.dir) for arg in args]
        for fws in fix_win_slashes:
            args[fws] = args[fws].replace('/', '\\')

        args = quote_args(args)

        cmd = args[0]
        args = args[1:]
        logger.debug('Shortcut cmd is %s, args are %s' % (cmd, args))
        workdir = self.shortcut.get('workdir', '')
        icon = self.shortcut.get('icon', '')

        workdir = substitute_env_variables(workdir, self.menu.dir)
        icon = substitute_env_variables(icon, self.menu.dir)

        # Fix up the '/' to '\'
        workdir = workdir.replace('/', '\\')
        icon = icon.replace('/', '\\')

        # Create the working directory if it doesn't exist
        if workdir:
            if not isdir(workdir):
                os.makedirs(workdir)
        else:
            workdir = '%HOMEPATH%'

        # Menu link
        dst_dirs = [self.menu.path]

        # Desktop link
        if self.shortcut.get('desktop'):
            dst_dirs.append(self.menu.dir['desktop'])

        # Quicklaunch link
        if self.shortcut.get('quicklaunch') and 'quicklaunch' in self.menu.dir:
            dst_dirs.append(self.menu.dir['quicklaunch'])

        name_suffix = " ({})".format(self.menu.dir['env_name']) if self.menu.dir['env_name'] else ""
        for dst_dir in dst_dirs:
            name = substitute_env_variables(self.shortcut['name'], self.menu.dir)
            dst = join(dst_dir, name + name_suffix + '.lnk')
            if remove:
                rm_rf(dst)
            else:
                # The API for the call to 'create_shortcut' has 3
                # required arguments (path, description and filename)
                # and 4 optional ones (args, working_dir, icon_path and
                # icon_index).
                create_shortcut(
                    u'' + cmd,
                    u'' + name + name_suffix,
                    u'' + dst,
                    u' '.join(arg for arg in args),
                    u'' + workdir,
                    u'' + icon,
                )