Rewrite `AppxReparse.exe` to JNA

No need to have separate tool for that

GitOrigin-RevId: 3019b647cb3b9ce9c01967deace0e4fbed834302
This commit is contained in:
Ilya.Kazakevich
2021-09-04 01:51:37 +00:00
committed by intellij-monorepo-bot
parent 233e2b492f
commit b6a9f2268e
12 changed files with 84 additions and 346 deletions
Binary file not shown.
Binary file not shown.
@@ -1,145 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{FFD92D09-1024-48E0-B3AB-DA83171B8F42}</ProjectGuid>
<RootNamespace>AppxReparseTool</RootNamespace>
<WindowsTargetPlatformVersion>10.0.18362.0</WindowsTargetPlatformVersion>
<ProjectName>AppxReparse</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v141</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="appxreparse.c" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="AppxReparse.rc" />
</ItemGroup>
<ItemGroup>
<Image Include="..\jb.ico" />
</ItemGroup>
<ItemGroup>
<Text Include="README.txt" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="appxreparse.c">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="AppxReparse.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<Image Include="..\jb.ico">
<Filter>Resource Files</Filter>
</Image>
</ItemGroup>
<ItemGroup>
<Text Include="README.txt" />
</ItemGroup>
</Project>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
@@ -1,19 +0,0 @@
AppX apps are installed in "C:\Program Files\WindowsApps\".
You can't run them directly. Instead, you must use reparse point from
"%LOCALAPPDATA%\Microsoft\WindowsApps" (this folder is under the %PATH%)
Reparse point is the special structure on NTFS level that stores "reparse tag" (type) and some type-specific data.
When a user accesses such files, Windows redirects the request to the appropriate target.
So, files in "%LOCALAPPDATA%\Microsoft\WindowsApps" are reparse points to AppX apps, and AppX can only be launched via them.
But for Python, there can be a reparse point that points to Windows store, so Store is opened when Python is not installed.
There is no official way to tell if "python.exe" points to AppX python or AppX "Windows Store".
MS provides API (via DeviceIOControl) to read reparse point structure.
There is also a reparse point tag for "AppX link" in SDK.
Reparse data is undocumented, but it is just an array of wide chars with some unprintable chars at the beginning.
This tool reads reparse point info and tries to fetch AppX name, so we can see if it points to Store or not.
See https://youtrack.jetbrains.com/issue/PY-43082
Output is unicode 16-LE
@@ -1,93 +0,0 @@
#include <Windows.h>
#include <wchar.h>
#include <stdio.h>
// Accepts path to AppX reparse point in "%LOCALAPPDATA%\Microsoft\WindowsApps"
// Returns name of AppX or error if wrong reparse point
typedef struct
{
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
wchar_t DATA[1024]; //1024 wide chars should be enough for AppX name
} REPARSE_DATA_BUFFER;
static DWORD _ProcessError(const wchar_t* place)
{
const DWORD error = GetLastError();
fwprintf(stderr, L"%ls: error %ld", place, error);
return error;
}
int wmain(int argc, wchar_t* argv[], wchar_t* envp[])
{
if (argc != 2)
{
fwprintf(stderr, L"Provide path to app");
return -1;
}
const wchar_t* path = argv[1];
HANDLE file = CreateFileW(
path,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT,
NULL);
if (file == INVALID_HANDLE_VALUE)
{
return _ProcessError(L"CreateFile");
}
REPARSE_DATA_BUFFER buffer;
ZeroMemory(&buffer, sizeof(buffer));
DWORD numOfBytes = 0;
if (!DeviceIoControl(
file,
FSCTL_GET_REPARSE_POINT,
NULL,
0,
&buffer,
sizeof(buffer),
&numOfBytes,
NULL
))
{
CloseHandle(file);
return _ProcessError(L"DeviceIoCtl");
}
if (numOfBytes == 0 || buffer.ReparseTag != IO_REPARSE_TAG_APPEXECLINK)
{
fwprintf(stderr, L"Not a reparse point");
CloseHandle(file);
return -1;
}
const WCHAR* textStart = buffer.DATA;
// Data consists of several unprintable (<32) chars following human-readable name of AppX
// Search for first printable (>32) char.
USHORT firstPrintableChar;
const USHORT maxChars = buffer.ReparseDataLength / sizeof(WCHAR);
for (firstPrintableChar = 0; (!iswprint(*textStart)) && firstPrintableChar < maxChars; firstPrintableChar++,
textStart++)
{
}
if (firstPrintableChar == maxChars)
{
fwprintf(stderr, L"No printable chars in data");
CloseHandle(file);
return -1;
}
//stdlib wprintf will convert chars to 1-byte charset, which may break non-ascii chars (if any)
// we print wide chars (16 bit LE) directly to stdout
const DWORD textLenInBytes = wcsnlen_s(textStart, maxChars - firstPrintableChar) * sizeof(WCHAR);
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), textStart, textLenInBytes, &numOfBytes, NULL);
CloseHandle(file);
return 0;
}
@@ -1,16 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by AppxReparse.rc
//
#define IDI_ICON1 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
-10
View File
@@ -18,8 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.txt = README.txt
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AppxReparseTool", "AppxReparseTool\AppxReparseTool.vcxproj", "{FFD92D09-1024-48E0-B3AB-DA83171B8F42}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
elevShared\elevShared.vcxitems*{45d41acc-2c3c-43d2-bc10-02aa73ffc7c7}*SharedItemsImports = 9
@@ -49,14 +47,6 @@ Global
{9D71A73C-2570-488A-958B-A126F4EEECB7}.Release|x64.Build.0 = Release|x64
{9D71A73C-2570-488A-958B-A126F4EEECB7}.Release|x86.ActiveCfg = Release|Win32
{9D71A73C-2570-488A-958B-A126F4EEECB7}.Release|x86.Build.0 = Release|Win32
{FFD92D09-1024-48E0-B3AB-DA83171B8F42}.Debug|x64.ActiveCfg = Debug|x64
{FFD92D09-1024-48E0-B3AB-DA83171B8F42}.Debug|x64.Build.0 = Debug|x64
{FFD92D09-1024-48E0-B3AB-DA83171B8F42}.Debug|x86.ActiveCfg = Debug|Win32
{FFD92D09-1024-48E0-B3AB-DA83171B8F42}.Debug|x86.Build.0 = Debug|Win32
{FFD92D09-1024-48E0-B3AB-DA83171B8F42}.Release|x64.ActiveCfg = Release|x64
{FFD92D09-1024-48E0-B3AB-DA83171B8F42}.Release|x64.Build.0 = Release|x64
{FFD92D09-1024-48E0-B3AB-DA83171B8F42}.Release|x86.ActiveCfg = Release|Win32
{FFD92D09-1024-48E0-B3AB-DA83171B8F42}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+1 -6
View File
@@ -1,6 +1,3 @@
This project consists of two tools: "launcher and elevator" and "appxreparse".
Launcher and elevator:
With UAC enabled, even an administrator has a limited access token and can't modify some folders like "Program Files".
@@ -20,14 +17,12 @@ But if std(out|err|in) are redirected to files or pipes, attaching to a console
In this case, the launcher creates named pipes, the elevator connects to them and provides their handlers as handlers for the newly created process.
The launcher then creates threads to read/write them to the console.
For "AppxReparse" see README inside the project.
-------
How to build.
ToolChain: VisualStudio 2017
SDK: 8.1 (for "launcher and elevator")
Win10 10.0.18362 (for "appx reparse")
You may open .sln from Visual Studio or use msbuild from VS command prompt:
msbuild Elevator.sln /p:Configuration=release /property:Platform=x86
@@ -56,5 +51,5 @@ Check no error
--------
How to check errors.
Both tools report errors as error codes, stderr and event log under "Application".
Tool reports errors as error codes, stderr and event log under "Application".
No event ids are registered, but you can check eventId and find an appropriate ReportEvent call.
@@ -78,5 +78,6 @@
<orderEntry type="library" name="kotlinx-coroutines-jdk8" level="project" />
<orderEntry type="library" name="jcef" level="project" />
<orderEntry type="library" name="Velocity" level="project" />
<orderEntry type="library" name="jna" level="project" />
</component>
</module>
@@ -1,11 +1,16 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.flavors
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.SystemInfo
import com.sun.jna.platform.win32.Kernel32
import com.sun.jna.platform.win32.Kernel32.*
import com.sun.jna.platform.win32.Ntifs
import com.sun.jna.platform.win32.WinioctlUtil
import com.sun.jna.ptr.IntByReference
import java.io.File
import java.io.FilenameFilter
import java.util.concurrent.TimeUnit
import java.nio.ByteBuffer
/**
* AppX packages installed to AppX volume (see ``Get-AppxDefaultVolume``, ``Get-AppxPackage``).
@@ -18,7 +23,7 @@ import java.util.concurrent.TimeUnit
* But when executed, they are processed by NTFS filter and redirected to their real location in AppX volume.
*
* There may be ``python.exe`` there, but it may point to Windows Store (so it can be installed when accessed) or to the real python.
* There is no Java API to see reparse point destination, so we use native app (See ``appxreparse.c`` and README in the same folder).
* There is no Java API to see reparse point destination, so we use JNA.
* This tool returns AppX name (either ``PythonSoftwareFoundation...`` or ``DesktopAppInstaller..``).
* We use it to check if ``python.exe`` is real python or WindowsStore mock.
*/
@@ -42,23 +47,19 @@ fun getAppxFiles(expectedProduct: String, filePattern: Regex): Collection<File>
?.filterKeys { expectedProduct in it }
?.values ?: emptyList()
/**
* If file is AppX reparse point link -- return it's product name
* If file is AppX reparse point link -- return its product name
*/
val File.appxProduct: String?
get() {
if (parentFile?.equals(userAppxFolder) != true) return null
val reparseTool = PathManager.findBinFile("AppxReparse.exe")!!
// Intellij API prohibits running external processes under Read action or on EDT, so we use java api as it is done for registry
val process = Runtime.getRuntime().exec(arrayOf(reparseTool.toFile().absolutePath, absolutePath))
// It is much faster in most cases, but since this code may run under EDT we limit it
if (!process.waitFor(1, TimeUnit.SECONDS)) return null
if (process.exitValue() != 0) return null
// appxreparse outputs wide chars (2 bytes), they are LE on Intel
val output = process.inputStream.readBytes().toString(Charsets.UTF_16LE)
return if (storeMarker !in output) output else null
return getAppxTag(absolutePath)?.let {
if (storeMarker !in it) it else null
}
}
/**
* Path to ``%LOCALAPPDATA%\Microsoft\WindowsApps``
*/
@@ -72,3 +73,71 @@ private val userAppxFolder =
if (appsPath.exists()) appsPath else null
}
}
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c8e77b37-3909-4fe6-a4ea-2b9d423b1ee4
private const val IO_REPARSE_TAG_APPEXECLINK = 0x8000001B
/**
* AppX apps are installed in "C:\Program Files\WindowsApps\".
You can't run them directly. Instead, you must use reparse point from
"%LOCALAPPDATA%\Microsoft\WindowsApps" (this folder is under the %PATH%)
Reparse point is the special structure on NTFS level that stores "reparse tag" (type) and some type-specific data.
When a user accesses such files, Windows redirects the request to the appropriate target.
So, files in "%LOCALAPPDATA%\Microsoft\WindowsApps" are reparse points to AppX apps, and AppX can only be launched via them.
But for Python, there can be a reparse point that points to Windows store, so Store is opened when Python is not installed.
There is no official way to tell if "python.exe" points to AppX python or AppX "Windows Store".
MS provides API (via DeviceIOControl) to read reparse point structure.
There is also a reparse point tag for "AppX link" in SDK.
Reparse data is undocumented, but it is just an array of wide chars with some unprintable chars at the beginning.
This tool reads reparse point info and tries to fetch AppX name, so we can see if it points to Store or not.
See https://youtrack.jetbrains.com/issue/PY-43082
Output is unicode 16-LE
*/
private fun getAppxTag(path: String): String? {
if (!SystemInfo.isWin10OrNewer) return null
val kernel = INSTANCE
val logger = Logger.getInstance(Kernel32::class.java)
val file = kernel.CreateFile(path, GENERIC_READ, FILE_SHARE_READ, null, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, null)
if (file == INVALID_HANDLE_VALUE) {
logger.warn("Invalid handle for $path")
return null
}
val buffer = Ntifs.REPARSE_DATA_BUFFER()
val bytesRead = IntByReference()
if (!kernel.DeviceIoControl(file, WinioctlUtil.FSCTL_GET_REPARSE_POINT, null, 0, buffer.pointer, buffer.size(), bytesRead, null)) {
logger.warn("DeviceIoControl error ${kernel.GetLastError()}")
return null
}
if (bytesRead.value < 1) {
logger.warn("0 bytes read")
return null
}
buffer.read()
if (buffer.ReparseTag != IO_REPARSE_TAG_APPEXECLINK.toInt()) {
logger.warn("Wrong tag ${buffer.ReparseTag}")
return null
}
// output is array of LE 2 bytes chars: \0\0\0[text]\0[junk]
val charBuffer = Charsets.UTF_16LE.decode(ByteBuffer.wrap(buffer.u.genericReparseBuffer.DataBuffer))
var from = 0
var to = 0
var startFound = false
for ((i, char) in charBuffer.withIndex()) {
val validChar = Character.getType(char) != Character.CONTROL.toInt()
if (validChar && !startFound) {
from = i
startFound = true
}
if (!validChar && startFound) {
to = i
break
}
}
return charBuffer.substring(from, to)
}