I'm trying to retrieve file information (specifically info about the icon) using SHGetFileInfo. In reality, I don't have the full path of the file, I only have the pidl.
The following code returns (0L, (0, 0, 0, '', '')) and my question is why.
from win32com.shell import shell, shellcon
def get_info():
desktop = shell.SHGetDesktopFolder()
eaten, desktop_pidl, attr = desktop.ParseDisplayName(None, None, r"C:\Users\Ella\Desktop")
return shell.SHGetFileInfo(desktop_pidl, 0, shellcon.SHGFI_PIDL | shellcon.SHGFI_SYSICONINDEX | shellcon.SHGFI_ICON | shellcon.SHGFI_DISPLAYNAME)
On the other hand, the code bellow does work for some reason (it uses full path instead of pidl):
from win32com.shell import shell, shellcon
def get_info2():
return shell.SHGetFileInfo(r"C:\Users\Ella\Desktop", 0, shellcon.SHGFI_SYSICONINDEX | shellcon.SHGFI_ICON | shellcon.SHGFI_DISPLAYNAME)
Thanks!
You've uncovered a bug in PySHGetFileInfo. If SHGFI_PIDL is set in flags, it calls PyObject_AsPIDL and stores the result to pidl_or_name, but it mistakenly passes name to SHGetFileInfo, which in this case is the initial NULL value. See below for more details.
You asked how to set a breakpoint on shell32!SHGetFileInfoW. There's no simple answer to that. Instead allow me to share an overview of what I did to test this. Hopefully this will at least get you started.
Test environment:
64-bit Windows 7 SP1 (6.1.7601)
Windows SDK 7.1 (ensure the debuggers are installed)
Visual Studio 2010 SP1
Visual C++ 2010 SP1 Compiler Update
Python 3.4 (and debug files)
Mercurial (hg.exe, not TortoiseHg)
Set up the shell environment.
"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.Cmd"
set MSSdk=%WindowsSDKDir%
set SYMDIR=C:\Symbols
set SYMSRV=http://msdl.microsoft.com/download/symbols
set _NT_SYMBOL_PATH=symsrv*symsrv.dll*%SYMDIR%*%SYMSRV%
path C:\Program Files\Debugging Tools for Windows (x64);%PATH%
path C:\Program Files\Mercurial;%PATH%
Create a Python virtual environment.
py -3.4 -m venv --symlinks test
venv doesn't link the .pdb files, so grab those manually in a for loop.
set PYDIR="%ProgramW6432%\Python34"
set CMD=mklink "test\Scripts\%~nxf" "%f"
for /R %PYDIR% %f in (*.pdb) do #%CMD%
Activate the virtual environment.
test\Scripts\activate
Clone the PyWin32 repo. Build and install version 219.
set HGSRV=http://pywin32.hg.sourceforge.net
hg clone %HGSRV%/hgroot/pywin32/pywin32
cd pywin32
hg up b219
I edited setup.py to comment out everything related to building
win32com.mapi. My setup didn't even have the required headers,
and when I obtained them there were problems building the
extension for WIN64.
Build and install the package.
python setup3.py install
Run Python under the console debugger, cdb.exe.
>cdb -xi ld python
Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: python
Symbol search path is: symsrv*symsrv.dll*C:\Symbols*
http://msdl.microsoft.com/download/symbols
Executable search path is:
(d50.1174): Break instruction exception - code 80000003 (first chance)
ntdll!LdrpDoDebuggerBreak+0x30:
00000000`770bcb70 cc int 3
0:000> bp shell32!SHGetFileInfoW
0:000> g
Python 3.4.2 (v3.4.2:ab2c023a9432, Oct 6 2014, 22:16:31)
[MSC v.1600 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
The option -xi ld in the above command line sets a filter to ignore printing loaded modules. There are lots of tutorials and 'cheat sheets' online for using Microsoft's debuggers such as WinDbg, cdb, and kd. The debuggers all use the same engine, so they support a common set of debugging commands.
The attached debugger has a breakpoint set on shell32!SHGetFileInfoW. When the breakpoint is triggered, the debugger grabs the console. One of the few redeeming features of the Windows console is its per-application input history and aliases. This makes it convenient to recall commands when bouncing in and out of the debugger and debuggee in the same console window.
>>> import os
>>> from win32com.shell import shell, shellcon
>>> print(shell.__file__)
C:\Temp\test\lib\site-packages\win32comext\shell\shell.pyd
>>> path = os.path.expanduser(r'~\Desktop\desktop.ini')
>>> pidl = shell.SHParseDisplayName(path, 0, None)[0]
>>> flags = (shellcon.SHGFI_PIDL |
... shellcon.SHGFI_SYSICONINDEX |
... shellcon.SHGFI_ICON |
... shellcon.SHGFI_DISPLAYNAME)
>>> shell.SHGetFileInfo(pidl, 0, flags)
Breakpoint 0 hit
SHELL32!SHGetFileInfoW:
000007fe`fd692290 fff3 push rbx
0:000> k 5
*** WARNING: Unable to verify checksum for
C:\Temp\test\lib\site-packages\win32comext\shell\shell.pyd
Child-SP RetAddr Call Site
00000000`003ff2d8 00000000`5f44c5e8 SHELL32!SHGetFileInfoW
00000000`003ff2e0 00000000`5f5af8bd shell!PySHGetFileInfo+0xf8
00000000`003ff610 00000000`5f62385b python34!PyCFunction_Call+0x12d
00000000`003ff640 00000000`5f625c89 python34!call_function+0x2ab
00000000`003ff6a0 00000000`5f62770c python34!PyEval_EvalFrameEx+0x2279
0:000> r rcx
rcx=0000000000000000
0:000> g
(0, (0, 0, 0, '', ''))
In the Windows x64 ABI, the first argument of a function is passed in register rcx. We know from the SHGetFileInfo docs that this should be the PIDL, but actually NULL is being passed. Clearly this is a bug. The stack trace lays the blame on shell!PySHGetFileInfo. Here's a snippet of the problematic code:
if (flags & SHGFI_PIDL) {
ok = PyObject_AsPIDL(obName, &pidl, FALSE);
pidl_or_name = (TCHAR *)pidl;
} else {
ok = PyWinObject_AsTCHAR(obName, &name, FALSE);
pidl_or_name = name;
}
if (!ok)
return NULL;
SHFILEINFO info;
memset(&info, 0, sizeof(info));
info.dwAttributes = info_attrs;
PY_INTERFACE_PRECALL;
DWORD_PTR dw = SHGetFileInfo(name, attr, &info, sizeof(info), flags);
The mistake is passing name as the first argument instead of pidl_or_name.
The question is tagged ctypes. IMO, using ctypes is worth it if doing so eliminates a large dependency such as PyWin32. I wouldn't normally use ctypes by itself for a COM-based API. The comtypes package builds on ctypes if you want to try that. In this case directly calling COM methods can be avoided by instead calling SHParseDisplayName. Other than using HRESULT return codes, it's pretty much like any other Win32 API.
import types as _types
import ctypes as _ctypes
from ctypes import wintypes as _wtypes
_mtypes = _types.ModuleType('_mtypes')
_ole32 = _ctypes.WinDLL('ole32')
_shell32 = _ctypes.WinDLL('shell32')
_user32 = _ctypes.WinDLL('user32')
try:
from win32com.shell import shell as _shell
except ImportError:
_shell = None
try:
from win32com.shell import shellcon
except ImportError:
shellcon = _types.ModuleType('shellcon')
shellcon.SHGFI_LARGEICON = 0x00000
shellcon.SHGFI_SMALLICON = 0x00001
shellcon.SHGFI_OPENICON = 0x00002
shellcon.SHGFI_SHELLICONSIZE = 0x00004
shellcon.SHGFI_PIDL = 0x00008
shellcon.SHGFI_USEFILEATTRIBUTES = 0x00010
shellcon.SHGFI_ICON = 0x00100
shellcon.SHGFI_DISPLAYNAME = 0x00200
shellcon.SHGFI_TYPENAME = 0x00400
shellcon.SHGFI_ATTRIBUTES = 0x00800
shellcon.SHGFI_ICONLOCATION = 0x01000
shellcon.SHGFI_EXETYPE = 0x02000
shellcon.SHGFI_SYSICONINDEX = 0x04000
shellcon.SHGFI_LINKOVERLAY = 0x08000
shellcon.SHGFI_SELECTED = 0x10000
shellcon.SHGFI_ATTR_SPECIFIED = 0x20000
try:
import win32con
except ImportError:
win32con = _types.ModuleType('win32con')
win32con.MAX_PATH = 260
win32con.FILE_ATTRIBUTE_READONLY = 0x00001
win32con.FILE_ATTRIBUTE_HIDDEN = 0x00002
win32con.FILE_ATTRIBUTE_SYSTEM = 0x00004
win32con.FILE_ATTRIBUTE_DIRECTORY = 0x00010
win32con.FILE_ATTRIBUTE_ARCHIVE = 0x00020
win32con.FILE_ATTRIBUTE_DEVICE = 0x00040
win32con.FILE_ATTRIBUTE_NORMAL = 0x00080
win32con.FILE_ATTRIBUTE_TEMPORARY = 0x00100
win32con.FILE_ATTRIBUTE_ATOMIC_WRITE = 0x00200
win32con.FILE_ATTRIBUTE_SPARSE_FILE = 0x00200
win32con.FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
win32con.FILE_ATTRIBUTE_XACTION_WRITE = 0x00400
win32con.FILE_ATTRIBUTE_COMPRESSED = 0x00800
win32con.FILE_ATTRIBUTE_OFFLINE = 0x01000
win32con.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x02000
win32con.FILE_ATTRIBUTE_ENCRYPTED = 0x04000
win32con.FILE_ATTRIBUTE_VIRTUAL = 0x10000
_mtypes.CData = _ctypes.Array.__bases__[0]
_mtypes.PPIDLIST_ABSOLUTE = _ctypes.POINTER(_ctypes.c_void_p)
_mtypes.SFGAOF = _wtypes.ULONG
_mtypes.PSFGAOF = _ctypes.POINTER(_mtypes.SFGAOF)
_ole32.CoInitialize.restype = _ctypes.HRESULT # checked
_ole32.CoInitialize.argtypes = (_ctypes.c_void_p,)
_ole32.CoUninitialize.restype = None
_ole32.CoUninitialize.argtypes = ()
_ole32.CoTaskMemFree.restype = None
_ole32.CoTaskMemFree.argtypes = (_ctypes.c_void_p,)
_user32.DestroyIcon.argtypes = (_wtypes.HICON,)
_shell32.SHParseDisplayName.restype = _ctypes.HRESULT # checked
_shell32.SHParseDisplayName.argtypes = (
_wtypes.LPCWSTR, # pszName, _In_
_ctypes.c_void_p, # pbc, _In_opt_
_mtypes.PPIDLIST_ABSOLUTE, # ppidl, _Out_
_mtypes.SFGAOF, # sfgaoIn, _In_
_mtypes.PSFGAOF) # psfgaoOut, _Out_opt_
class SHFILEINFO(_ctypes.Structure):
_fields_ = (('hIcon', _wtypes.HICON),
('iIcon', _ctypes.c_int),
('dwAttributes', _wtypes.DWORD),
('szDisplayName', _wtypes.WCHAR * win32con.MAX_PATH),
('szTypeName', _wtypes.WCHAR * 80))
_mtypes.SHFILEINFO = SHFILEINFO
_mtypes.PSHFILEINFO = _ctypes.POINTER(SHFILEINFO)
_shell32.SHGetFileInfoW.restype = _ctypes.c_void_p
_shell32.SHGetFileInfoW.argtypes = (
_wtypes.LPVOID, # pszPath, _In_
_wtypes.DWORD, # dwFileAttributes,
_mtypes.PSHFILEINFO, # psfi, _Inout_
_wtypes.UINT, # cbFileInfo,
_wtypes.UINT) # uFlags
def SHGetFileInfo(pidl, attributes=0, flags=0):
if _shell is not None:
if not isinstance(pidl, (str, bytes, _mtypes.CData)):
pidl = _shell.PIDLAsString(pidl)
finfo = SHFILEINFO()
_ole32.CoInitialize(None)
try:
retval = _shell32.SHGetFileInfoW(pidl,
attributes,
_ctypes.byref(finfo),
_ctypes.sizeof(finfo),
flags)
finally:
_ole32.CoUninitialize()
if not retval:
if flags != shellcon.SHGFI_EXETYPE:
raise _ctypes.WinError()
return retval, finfo
Example:
if __name__ == '__main__':
import os
path = os.path.expanduser(r'~\Desktop\desktop.ini')
pidl = _shell.SHParseDisplayName(path, 0)[0]
assert isinstance(pidl, list)
flags = (shellcon.SHGFI_PIDL |
shellcon.SHGFI_ICON |
shellcon.SHGFI_DISPLAYNAME |
shellcon.SHGFI_TYPENAME |
shellcon.SHGFI_ATTRIBUTES |
shellcon.SHGFI_SYSICONINDEX)
hImageList, finfo = SHGetFileInfo(pidl, 0, flags)
print('hImageList:', hImageList)
for name, typ in finfo._fields_:
print(name, ': ', ascii(getattr(finfo, name)), sep='')
if finfo.hIcon:
_user32.DestroyIcon(finfo.hIcon)
Output:
hImageList: 4411024
hIcon: 10617107
iIcon: 7
dwAttributes: 1078497655
szDisplayName: 'desktop.ini'
szTypeName: 'Configuration settings'
Related
I'm trying to add my program to registry and this is my code...
def regc():
reg = windll.kernel32
print(reg)
hkey = 'HKEY_CURRENT_USER'
lsubkey = 'Software\Microsoft\Windows\CurrentVersion\Run'
reserved = 0
flag = 'REG_OPTION_BACKUP_RESTORE'
samdesired = 'KEY_ALL_ACCESS'
ipsec = None
handle = reg.RegCreateKeyExA(hkey, lsubkey, reserved, flag, samdesired, ipsec, None)
Its not giving me any errors but it still isn't creating a new key in registry. What am I doing wrong?
To use ctypes correctly, define .argtypes and .restype to do error checking of your parameters. Many of the types used are wrong. hkey, flag, and samdesired are not strings, for example. The return value is not a handle, but a status. The return value is an output parameter (pkhResult in the docs). You must read the documentation and examine the header files of all the variable definitions carefully.
Also, in Python 3 strings are Unicode, so use the W form of Windows APIs to accept Unicode strings. Use raw strings (r'...') for the subkey since it contains backslashes that could be interpreted as escape codes.
Here's a working example:
from ctypes import *
from ctypes import wintypes as w
# Values found from reading RegCreateKeyExW documentation,
# using Go To Definition on the types in Visual Studio,
# and printing constants in a C program, e.g. printf("%lx\n",KEY_ALL_ACCESS);
HKEY = c_void_p
PHKEY = POINTER(HKEY)
REGSAM = w.DWORD
LPSECURITY_ATTRIBUTES = c_void_p
LSTATUS = w.LONG
# Disposition values
REG_CREATED_NEW_KEY = 0x00000001
REG_OPENED_EXISTING_KEY = 0x00000002
ERROR_SUCCESS = 0
HKEY_CURRENT_USER = c_void_p(0x80000001)
REG_OPTION_NON_VOLATILE = 0
KEY_ALL_ACCESS = 0x000F003F
dll = WinDLL('kernel32')
dll.RegCreateKeyExW.argtypes = HKEY,w.LPCWSTR,w.DWORD,w.LPWSTR,w.DWORD,REGSAM,LPSECURITY_ATTRIBUTES,PHKEY,w.LPDWORD
dll.RegCreateKeyExW.restype = LSTATUS
hkey = HKEY_CURRENT_USER
lsubkey = r'Software\Microsoft\Windows\CurrentVersion\Run'
options = REG_OPTION_NON_VOLATILE
samdesired = KEY_ALL_ACCESS
# Storage for output parameters...pass by reference.
handle = HKEY()
disp = w.DWORD()
status = dll.RegCreateKeyExW(HKEY_CURRENT_USER, lsubkey, 0, None, options, samdesired, None, byref(handle), byref(disp))
if status == ERROR_SUCCESS:
print(f'{disp=} {handle=}')
Output:
disp=c_ulong(2) handle=c_void_p(3460)
The disposition value of 2 indicates the key already exists (REG_OPENED_EXISTING_KEY).
You could also install pywin32 and use win32api.RegCreateKey or win32api.RegCreateKeyEx where all the work is already done for you.
I have an almost working SConstruct file. I'm not using any SConscript files currently, and would prefer not to need any in my source repositories (git, not SCons).
Quick summary -- my problem occurs when changing some arguments, then returning to the previous arguments, the same files are rebuilt.
I run scons -f Builder_repo/SConstruct 'NameOfTargetLibrary.b' to build a library, NameOfTargetLibrary.b from NameOfTargetLibrary.src.
<lib>.b should be placed in a location that depends on various flags (Debug/Release, 32/64 bit, platform(from list)) like so:
topdir
|\-- Builder_repo (containing SConstruct, site_scons\...)
|\-- Lib1 (contains lib1.src, bunch of source files)
|\-- Lib2 (lib2.src, lib2 sources)
\--- BuiltLibs
|\-- Windows
| |\-- Release_32
| | |\-- lib1.b
| | |\-- lib2.b
| | \--- libn.b
| |\-- Debug_64
| | |\-- lib1.b
| | |\-- lib2.b
| | \--- libn.b
| \--- (Debug_32, Release_64)
\--- (etc, other targets, currently just one)
The command line is something like (split to multiple lines for readability, but only one line in SCons/cmdLine)
"abspath to exe" "static path and args" --x64 --
"abspath(lib1.src)" "abspath(BuiltLibs)"
"abspath(BuiltLibs/Windows/Release_64)"
"flags for debug/release, target, bitness"
The 'working' SConstruct uses a tool with a generate(env) something like:
construct target directory (e.g. BuiltLibs\Windows\Release_32) Store in env.
search for .src files
get containing directory (using os.path.dirname)
add to env.Repositories(dirname(lib.src))
tgt = env.BLDR(<TARGETDIR>/lib.b, lib.src)
env.Alias(lib.b, tgt)
The Builder then uses an Emitter to add to the source list any <TARGETDIR>/libx.b files on which lib.src depends (read from a source file). These could instead be added as just libx.b if preferable?
The Generator parses the input target and source lists to form the command line, which it returns. With the current configuration, target and source are both relative paths, so probably the Repository calls are unnecessary.
When I run
scons -f Builder_repo\SConstruct 'lib2.b' DEBUG=0 BITNESS=32 TRGT=Windows
(lib2.src depends on lib1.b, due to the emitter), the correct lib1.b and lib2.b are built and placed in BuiltLibs\Windows\Release_32\lib{1,2}.b.
If I repeat the command, then nothing is built and 'lib2.b is up to date'.
Then, I try scons -f <..> 'lib2.b' DEBUG=1 <other args same>. Both libraries are built and placed in BuiltLibs\Windows\Debug_32\lib{1,2}.b as expected.
When I then try the first command again (DEBUG=0) I expect nothing to be built (the lib1.b, lib2.b are still up to date - no sources changed and the previously built files are still in Release_32) but instead they are rebuilt.
I tried to solve this problem by returning a reduced command line when for_signature is true, such that the value returned in that case is more like:
"abspath to exe" "static path and args" --
"abspath(lib1.src)" "abspath(BuiltLibs)" "version string"
where "version string" is something not affected by the debug/release, 32/64, platform flags (but does change with the source code). This made seemingly no difference.
I tried some variations on this using env.VariantDir(<TARGETDIR>, '.', duplicate=0) and then tgt = env.BLDR(lib.b, Lib1/lib.src), env.Alias(<TARGETDIR>/lib.b, tgt) or similar, but I haven't managed to improve anything (some configurations just made it always rebuilt, others made it so the dependencies couldn't be found and SCons errored.
How should I be doing this?
SConstruct:
import os
Decider('make')
Default(None)
# Add command line arguments with default values.
# These can be specified as, for example, LV_TARGET=cRIO
cmdVars = Variables(None, ARGUMENTS)
cmdVars.AddVariables(
EnumVariable('LV_TARGET', 'Choose the target for LabVIEW packages',
'Windows', allowed_values=('Windows', 'cRIO')),
BoolVariable('DEBUG', 'Set to 1 to build a debug-enabled package', 0),
EnumVariable('BITNESS', 'Choose the bitness for LabVIEW packages',
'32', allowed_values=('32', '64')),
EnumVariable('LV_VER', 'Choose the version of LabVIEW to use',
'2017', allowed_values=('2017',))
)
# Define a list of source extensions
src_exts = ['.vi', '.ctl', '.lvlib', '.vim', '.vit']
env = Environment(variables = cmdVars, ENV = os.environ, tools=['PPL'], PPLDIR='PPLs', SRC_EXTS=' '.join(src_exts))
init.py for the PPL tool:
""" SCons.Tool.PPL
Tool-specific initialization for compilation of lvlibp files from lvlib files,
using the Wiresmith 'labview-cli.exe' and the LabVIEW code stored in the
PPL_Builder GitHub repository.
This module should not usually be imported directly.
It can be imported using a line in SConstruct or SConscript like
env = Environment(tools=['PPL'])
"""
# A reference for this code can be found at
# https://github.com/SCons/scons/wiki/ToolsForFools
# which describes the creation of a Tool for JALv2 compilation.
import SCons.Builder
from SCons.Script import GetOption
import SCons.Node
import SCons.Util
import os.path
import textwrap
import re
import contextlib
import subprocess
# Add warning classes
class ToolPPLWarning(SCons.Warnings.Warning):
pass
class LabVIEW_CLI_ExeNotFound(ToolPPLWarning):
pass
SCons.Warnings.enableWarningClass(ToolPPLWarning)
__verbose = False
class LV_BuildPaths:
""" A simple class to contain the build paths
and configuration flags for PPL compilation
Contains the attributes:
hwTarget{,Dir}, debug{Opt,Flag,String}, bitness{,Flag}, lv_ver,
{ppl,storage,copy,topData,data}Dir
"""
def __init__(self, env):
# Set the target parameters
self.hwTarget = env.get('LV_TARGET')
copyDirExtra = ""
if self.hwTarget == "cRIO":
self.hwTargetDir = "cRIO-9045"
copyDirExtra = os.path.join('home','lvuser','natinst','bin')
else:
self.hwTargetDir = self.hwTarget
# Set the debug parameters
self.debugOpt = env.get('DEBUG')
self.debugFlag = int(self.debugOpt)
self.debugString = "Debug" if self.debugOpt else "Release"
# Set the bitness parameters
self.bitness = env.get('BITNESS')
self.bitnessFlag = ''
if self.bitness == '64':
self.bitnessFlag = '--x64'
# Get the LabVIEW year
self.lv_ver = env.get('LV_VER')
# Get important build directory paths.
# PPL files should be searched for in storageDir
self.pplDir = os.path.normpath(env.get('PPLDIR', 'PPLs'))
self.storageDir = os.path.join(self.pplDir, self.hwTargetDir, f'{self.debugString}_{self.bitness}', copyDirExtra)
self.copyDir = os.path.join(self.pplDir, self.hwTargetDir, copyDirExtra)
self.topDataDir = os.path.join(self.pplDir, 'Data')
self.dataDir = os.path.join(self.copyDir, 'Data')
def __str__(self):
return (textwrap.dedent(f"""\
The directories are as follows...
PPL Dir: {self.pplDir}
Storage Dir: {self.storageDir}
Copy Dir: {self.copyDir}""")
)
def _print_info(message):
""" Disable the output of messages if '--quiet', '-s' or '--silent'
are passed to the command line """
if not GetOption('silent'):
print(message)
def _detectCLI(env):
""" Search for the labview-cli.exe installed by Wiresmith's VIPackage """
try:
# If defined in the environment, use this
_print_info(f"labview-cli defined in the environment at {env['LV_CLI']}")
return env['LV_CLI']
except KeyError:
pass
cli = env.WhereIs('labview-cli')
if cli:
_print_info(f"Found labview-cli at {cli}")
return cli
raise SCons.Errors.StopError(
LabVIEW_CLI_ExeNotFound,
"Could not detect the labview-cli executable")
return None
#contextlib.contextmanager
def pushd(new_dir):
previous_dir = os.getcwd()
os.chdir(new_dir)
yield
os.chdir(previous_dir)
def _getHash(env, dir):
if env['GIT_EXE']:
with pushd(dir):
#cmdLine = env['git_showref']
cmdLine = env['git_describe']
return subprocess.run(cmdLine, shell=True, capture_output=True, text=True).stdout
return ''
def _detectGit(env):
""" Search for a git executable. This is not required for usage """
git = None
try:
# If defined in the environment, use this
_print_info(f"git executable defined in the environment at {env['GIT_EXE']}")
git = env['GIT_EXE']
except KeyError:
pass
cli = env.WhereIs('git')
if cli:
_print_info(f"Found git at {cli}")
git = cli
if git:
hash_len = 12
env['GIT_EXE'] = f"'{git}'" # I edited this line compared to the version in the repository, but I don't think it's relevant.
env['git_describe'] = f'"{git}" describe --dirty="*" --long --tags --always --abbrev={hash_len}'
env['git_showref'] = f'"{git}" show-ref --hash={hash_len} --head head'
return None
#
# Builder, Generator and Emitter
#
def _ppl_generator(source, target, env, for_signature):
""" This function generates the command line to build the PPL.
It should expect to receive a target as a relative path
['<SD>/A.lvlibp'], and source will be either None, or
['<src>/A.lvlib'].
When for_signature == 0, the PPL will actually be built.
"""
# Get these parameters properly
run_vi = os.path.abspath(os.path.join('.','PPL_Builder','Call_Builder_Wiresmith.vi'))
cliOpts = ''
package_ver = "0.0.0.0#sconsTest"
# These are extracted from the environment
cli = env['LV_CLI']
bp = env['LV_Dirs']
ver = bp.lv_ver
pplDir = f'{os.path.abspath(bp.pplDir)}'
storageDir = f'{os.path.abspath(bp.storageDir)}'
# Dependencies are parsed for the command line. They are already dependencies of the target.
pplSrcs = source[1:]
depsString = ""
if pplSrcs:
if __verbose:
_print_info("Adding PPL dependencies: %s" % [ str(ppl) for ppl in pplSrcs ])
depsString = " ".join([f'"{os.path.basename(ppl.get_string(for_signature))}"' for ppl in pplSrcs])
cmdLine = f'"{cli}" --lv-ver {ver} {bp.bitnessFlag} {run_vi} {cliOpts} -- '
lvlib_relpath = str(source[0])
lvlib_abspath = os.path.abspath(lvlib_relpath)
git_ver = _getHash(env, os.path.dirname(lvlib_abspath))
print("git version is " + str(git_ver).strip())
argsLine = f'"{lvlib_abspath}" "{pplDir}" "{storageDir}" {bp.debugFlag} {bp.hwTarget} "{package_ver}" {depsString}'
if not for_signature:
_print_info(f"Making {lvlib_abspath}")
return cmdLine + argsLine
#return cmdLine + argsLine
def _ppl_emitter(target, source, env):
""" Appends any dependencies found in the .mk file to the list of sources.
The target should be like [<SD>/A.lvlibp],
and the source should be like [<src>/A.lvlib]
"""
if not source:
return target, source
exts_tuple = tuple(env['SRC_EXTS'].split(' '))
src_files = _get_other_deps(source, exts_tuple)
if __verbose:
_print_info("Adding " + str(src_files) + " as dependencies")
env.Depends(target, src_files)
depsList, nodeDepsList = _get_ppl_deps(str(source[0]), env)
if nodeDepsList:
source += [os.path.normpath(os.path.join(env['LV_Dirs'].storageDir, str(pplNode))) for pplNode in nodeDepsList]
return target, source
_ppl_builder = SCons.Builder.Builder(generator = _ppl_generator, emitter = _ppl_emitter)
def lvlibpCreator(env, target, source=None, *args, **kw):
""" A pseudo-Builder for the labview-cli executable
to build .lvlibp files from .lvlib sources, with
accompanying dependency checks on appropriate source files
Anticipate this being called via env.PPL('<SD>/A.lvlibp'),
where target is a string giving a relative path, or
env.PPL('<SD>/A.lvlibp', '<src>/A.lvlib')
"""
bPaths = env['LV_Dirs']
# Ensure that if source exists, it is a list
if source and not SCons.Util.is_List(source):
source = [source]
if __verbose:
_print_info(f"Target = {target}")
if source:
_print_info("Sources = %s" % [ str(s) for s in source])
if __verbose:
_print_info("args: %s" % [ str(s) for s in args ])
_print_info("kw: %s" % str(kw.items()))
tgt = _ppl_builder.__call__(env, target, source, **kw)
return tgt
def _scanForLvlibs(env, topdir=None):
# Maybe check this...
if not topdir:
topdir = '.'
bPaths = env['LV_Dirs']
lvlibList = []
for root, dirs, files in os.walk(topdir):
# if any of files ends with .lvlib, add to the list
lvlibList += map(lambda selected: os.path.join(root, selected), filter(lambda x: x[-6:] == '.lvlib', files))
for lib in lvlibList:
# Set up the possibility of building the lvlib
(srcDir, libnameWithExt) = os.path.split(lib)
# Add the source repository
if __verbose:
_print_info("Adding repository at: " + srcDir)
env.Repository(srcDir)
# Add the build instruction
lvlibpName = libnameWithExt + 'p'
tgt = env.PPL(os.path.normpath(os.path.join(bPaths.storageDir, lvlibpName)),lib)
if __verbose:
_print_info(f"Adding alias from {libnameWithExt+'p'} to {str(tgt)}")
env.Alias(lvlibpName, tgt)
def _get_ppl_deps(lvlib, env):
lvlib_s = str(lvlib)
lvlib_name = os.path.basename(lvlib_s)
mkFile = lvlib_s.replace('.lvlib','.mk')
if os.path.isfile(mkFile):
# load dependencies from file
depVarName = lvlib_name.replace(' ',r'\+').replace('.lvlib','_Deps')
f = open(mkFile, "r")
content = f.readlines() # Read all lines (not just first)
depsList = []
for line in content:
matchedDeps = re.match(depVarName+r'[ ]?:=[ ]?(.*)$', line)
if matchedDeps:
listDeps = matchedDeps.group(1).replace(r'\ ','+').split(' ')
depsList = ['"' + elem.replace('+', ' ') + '"' for elem in listDeps]
nodeList = [ env.File(elem.replace('+', ' ')) for elem in listDeps]
return (depsList, nodeList)
raise RuntimeError("Found a .mk file ({mkFile}) but could not parse it to get dependencies.")
#print(f"No .mk file for {lvlib_name}")
return ('', None)
def _get_other_deps(source, exts):
parent_dir = os.path.dirname(str(source[0]))
if __verbose:
_print_info(f"Searching {parent_dir} for source files...")
_print_info(f"Acceptable extensions are {exts}")
src_files = []
for root, dirs, files in os.walk(parent_dir):
src_files += [os.path.join(root, file) for file in files if file.endswith(exts)]
return src_files
def generate(env):
'''Add builders and construction variables to the Environment.'''
env['LV_CLI'] = _detectCLI(env)
env.AddMethod(lvlibpCreator, "PPL")
_detectGit(env)
bp = LV_BuildPaths(env)
_print_info(bp)
env['LV_Dirs'] = bp
# Search for all lvlib files
_scanForLvlibs(env)
def exists(env):
return _detectCLI(env)
As briefly described in the comments, the reason for the rebuilds was the use of Decider('make') (i.e. checking by timestamp) with the effective globbing of source files catching an autogenerated file.
This was easily seen when running scons --debug=explain as suggested by bdbaddog in the comments to the question.
Although slightly brittle, the simplest solution is to modify the emitter, leaving the following (see the ---> mark) :
def _ppl_emitter(target, source, env):
""" Appends any dependencies found in the .mk file to the list of sources.
The target should be like [<SD>/A.lvlibp],
and the source should be like [<src>/A.lvlib]
"""
if not source:
return target, source
exts_tuple = tuple(env['SRC_EXTS'].split(' '))
src_files = _get_other_deps(source, exts_tuple)
--->filtered_files = list(filter(lambda x: "Get PPL Version.vi" not in x, src_files))
if __verbose:
_print_info("Adding " + str(filtered_files) + " as dependencies")
env.Depends(target, filtered_files)
depsList, nodeDepsList = _get_ppl_deps(str(source[0]), env)
if nodeDepsList:
source += [os.path.normpath(os.path.join(env['LV_Dirs'].storageDir, str(pplNode))) for pplNode in nodeDepsList]
return target, source
By removing this file, the target no longer has an explicit dependency on the generated file (this is independent of the Decider call).
Additionally removing the Decider('make') line from the SConstruct file allows the entire source repository to be deleted and redownloaded without triggering rebuilds.
As a side note, the Git-specific code was also removed and placed inside the code called by the Builder - in this way, it is additionally (to the reduction of code benefits) only called if required for a rebuild (rather than every time SCons runs).
I would like to get the unix file type of a file specified by path (find out whether it is a regular file, a named pipe, a block device, ...)
I found in the docs os.stat(path).st_type but in Python 3.6, this seems not to work.
Another approach is to use os.DirEntry objects (e. g. by os.listdir(path)), but there are only methods is_dir(), is_file() and is_symlink().
Any ideas how to do it?
You use the stat module to interpret the result of os.stat(path).st_mode.
>>> import os
>>> import stat
>>> stat.S_ISDIR(os.stat('/dev/null').st_mode)
False
>>> stat.S_ISCHR(os.stat('/dev/null').st_mode)
True
You can make a general function to return the determined type. This works for both Python 2 and 3.
import enum
import os
import stat
class PathType(enum.Enum):
dir = 0 # directory
chr = 1 # character special device file
blk = 2 # block special device file
reg = 3 # regular file
fifo = 4 # FIFO (named pipe)
lnk = 5 # symbolic link
sock = 6 # socket
door = 7 # door (Py 3.4+)
port = 8 # event port (Py 3.4+)
wht = 9 # whiteout (Py 3.4+)
unknown = 10
#classmethod
def get(cls, path):
if not isinstance(path, int):
path = os.stat(path).st_mode
for path_type in cls:
method = getattr(stat, 'S_IS' + path_type.name.upper())
if method and method(path):
return path_type
return cls.unknown
PathType.__new__ = (lambda cls, path: cls.get(path))
>>> PathType('/dev/null')
<PathType.chr: 1>
>>> PathType('/home')
<PathType.dir: 0>
Python 3.6 has pathlib and its Path objects have methods:
is_dir()
is_file()
is_symlink()
is_socket()
is_fifo()
is_block_device()
is_char_device()
pathlib takes a bit to get used to (at least for me having come to Python from C/C++ on Unix), but it is a nice library
20 November 2020
The code supplied in the answer by 'Artyer' does not work in python 2.7.17, but appears to work in python 3.6.9 except for one detail:
The call:
path = os.stat(path).st_mode
should be:
path = os.lstat(path).st_mode
Otherwise you just get the regular file that a soft link points to.
For python 2 (and 3), if you wish to follow this style of coding, the following is better, with some name changes for clarity:
import enum,os,stat
class PathTypes(enum.Enum):
door = 0 # door (Py 3.4+)
port = 1 # event port (Py 3.4+)
wht = 2 # whiteout (Py 3.4+)
dir = 3 # directory
chr = 4 # character special device file
blk = 5 # block special device file
reg = 6 # regular file
fifo = 7 # FIFO (named pipe)
lnk = 8 # symbolic link
sock = 9 # socket
unimplemented = 10
def filetype(path):
mode=os.lstat(path).st_mode # do not follow links
for t in PathTypes:
try: func=getattr(stat, 'S_IS' + t.name.upper())
except AttributeError: continue
if func(mode): return t
return PathTypes["unimplemented"]
Notice the reordering that forces testing in python 2 to consider undefined stat functions and exercise the necessary try...except statement. The members are also renumbered since in python 2, enum.ENUM apparently sorts the members by value, and this is apparently undocumented. Since python 2 is now not supported, bug or not, that is the way it is.
The enum documentation recommends against using value 0 since that is boolean False and all members are supposed to be boolean True. The documentation also recommends using this simpler helper function style.
(https://cpython-test-docs.readthedocs.io/en/latest/library/enum.html)
To avoid some of these ambiguities the following will behave as documented, and also orders processing to return the most likely results quickly:
import enum,os,stat
members= ( \
('reg', 'S_ISREG' ), \
('dir', 'S_ISDIR' ), \
('lnk', 'S_ISLNK' ), \
('fifo', 'S_ISFIFO'), \
('sock', 'S_ISSOCK'), \
('chr', 'S_ISCHR' ), \
('blk', 'S_ISBLK' ), \
('door', 'S_ISDOOR'), \
('port', 'S_ISPORT'), \
('wht', 'S_ISWHT' ), \
('unimplemented', '') \
)
FileTypes=enum.Enum('FileTypes',members)
def filetype(path):
"""Get unix filetype:
reg,dir,lnk,fifo,sock,chr,blk
and for py3.4+:
door,port,wht.
'path' is a full pathname for some file."""
mode=os.lstat(path).st_mode # do not follow links
for t in FileTypes:
try: func=getattr(stat, t.value)
except AttributeError: continue
if func(mode): return t
return FileTypes["unimplemented"]
The enum.ENUM functional API apparently does not have the sorting bug and keeps the members in the order they are presented, as documented.
Given the troublesome nature of enum.ENUM, it is probably better simply to avoid it by using time
tested python primitives:
import os,stat
_stat_funcs=( \\
'S_ISREG','S_ISDIR','S_ISLNK','S_ISFIFO','S_ISSOCK', \\
'S_ISCHR','S_ISBLK','S_ISDOOR','S_ISPORT','S_ISWHT' )
# ^----- python 3.4+ only ----->|
# ^-- SOLARIS only --->|^--BSD->|
_ls_chr=( \\
'-' ,'d' ,'l' ,'p' ,'s' , \\
'c' ,'b' ,'D' ,'P' ,'w' )
# ^----- python 3.4+ only------>|
# ^-- SOLARIS only --->|^--BSD->|
_ftypes=tuple( (c,getattr(stat,f)) \\
for c,f in zip(_ls_chr,_stat_funcs) if f in dir(stat))
def filetype(path):
"""Get unix filetype designator used in 'ls' shell command listings:
reg('-'),dir('d'),lnk('l'),fifo('p'),sock('s'),chr('c'),blk('b')
and for py3.4+:
door('D'),port('P'),wht('w'). (solaris,solaris,BSD only)
'path' is a full pathname for some file. Returns 'u' for an
unknown or unimplemented filetype."""
mode=os.lstat(path).st_mode # do not follow links
for c,func in _ftypes:
if func(mode): return c
return 'u'
This is a lot more efficient than the code 'Artyer' offers, which is important when processing large numbers of files. Usage for both python2 and 3:
>>> filetype('/dev/null')
'c'
>>> filetype('/dev/sda')
'b'
>>> filetype('/home/test')
'd'
>>> filetype('/home/random.txt')
'-'
>>> filetype('/home/test/hlnk') # hard link
'-'
>>> filetype('/home/test/slnk') # soft link
'l'
>>> filetype('/home/test/sckt')
's'
>>> filetype('/home/test/fifo.pipe')
'p'
>>>
In Python, ctypes.util.find_library can be used to locate a library in a way similar to what the compiler does. In Mac OSX, the function returns a full pathname. But in linux, only the filename is returned. (Here are the docs )
Is there a way to obtain the fullpath also in linux?
You can load the library and the iterate over the loaded libraries using dl_iterate_phdr:
#!python
from ctypes import *
from ctypes.util import find_library
# this struct will be passed as a ponter,
# so we don't have to worry about the right layout
class dl_phdr_info(Structure):
_fields_ = [
('padding0', c_void_p), # ignore it
('dlpi_name', c_char_p),
# ignore the reset
]
# call back function, I changed c_void_p to c_char_p
callback_t = CFUNCTYPE(c_int,
POINTER(dl_phdr_info),
POINTER(c_size_t), c_char_p)
dl_iterate_phdr = CDLL('libc.so.6').dl_iterate_phdr
# I changed c_void_p to c_char_p
dl_iterate_phdr.argtypes = [callback_t, c_char_p]
dl_iterate_phdr.restype = c_int
def callback(info, size, data):
# simple search
if data in info.contents.dlpi_name:
print(info.contents.dlpi_name)
return 0
if __name__ == '__main__':
# the target lib we want to find
target_lib = find_library('xml2')
print(target_lib)
# load it
lib = CDLL(target_lib)
# iterate over the loaded libs
dl_iterate_phdr(callback_t(callback), target_lib)
for example:
$ python test.py
libxml2.so.2
/usr/lib/libxml2.so.2
$
find_library in Linux do things like this. Why we should not make it too?
import struct
def myfind(name):
# see ctypes.find_library code
uname = os.uname()[4]
if uname.startswith("arm"):
uname = "arm"
if struct.calcsize('l') == 4:
machine = uname + '-32'
else:
machine = uname + '-64'
mach_map = {
'x86_64-64': 'libc6,x86-64',
'ppc64-64': 'libc6,64bit',
'sparc64-64': 'libc6,64bit',
's390x-64': 'libc6,64bit',
'ia64-64': 'libc6,IA-64',
'arm-32': 'libc6(,hard-float)?',
}
abi_type = mach_map.get(machine, 'libc6')
# Note, we search libXXX.so.XXX, not just libXXX.so (!)
expr = re.compile(r'^\s+lib%s\.so.[^\s]+\s+\(%s.*=>\s+(.*)$' % (re.escape(name), abi_type))
p = subprocess.Popen(['ldconfig', '-N', '-p'], stdout=subprocess.PIPE)
result = None
for line in p.stdout:
res = expr.match(line)
if res is None:
continue
if result is not None:
raise RuntimeError('Duplicate library found for %s' % name)
result = res.group(1)
if p.wait():
raise RuntimeError('"ldconfig -p" failed')
if result is None:
raise RuntimeError('Library %s not found' % name)
return result
I have problem with getting the path of the executable from the process. I'm using Windoxs XP 64 bit with Python 2.7.5 x86_64 and the code is:
pbi = create_string_buffer(200)
size = c_int()
NTDLL.NtQueryInformationProcess.restype = c_int
ret = NTDLL.NtQueryInformationProcess(h_process, #previously defined
27,
byref(pbi),
sizeof(pbi),
byref(size))
fbuf = pbi.raw[8:]
fbuf = fbuf[:fbuf.find('\0\0')+1]
return fbuf.decode('utf16', errors="ignore")
This work on Windows XP 32 bit. Founding in MSDN, I found a dfferent mechanism using 'psapi.dll' and specifically GetProcessImageFileName. This method works on Windows 7 x64 and the code is:
PSAPI = ctypes.WinDLL('Psapi.dll')
GetProcessImageFileName = PSAPI.GetProcessImageFileNameA
GetProcessImageFileName.restype = ctypes.wintypes.DWORD
path = create_string_buffer(200)
ret = GetProcessImageFileName(self.h_process, path, 200)
Someone has any idea????
Thanks in advance