Console UWP Applications and File-system Access

News

Extraordinary Robot
Robot
As announced in the May 2018 issue of MSDN Magazine, the latest Windows update introduces support for Console UWP apps. We’re also introducing support for UWP apps to get broader access to the file-system than was previously possible. In fact, it was during the design and development phases of the Console app support that we realized we’d need to provide these apps with better file-system access – so the two features complement each other nicely.

Console apps represent a significant departure from the “normal” UWP model. Specifically, these apps don’t have their own windows, and don’t use the familiar input mechanisms. One of the attractions of UWP is the rich UI support, which allows you to build beautiful, highly-interactive apps. Console apps, on the other hand, use the standard console window for input and output, and the model is very much command-line driven and plain-text-based.

So, yes, Console apps are very different from traditional UWP apps, and they meet the requirements of a rather different target audience. They’re aimed squarely at developers who need to build admin or developer tools. To help you get started, we published a set of Console App (Universal) Project Templates in the Visual Studio Marketplace.

Let’s walk through creating a very simple Console UWP app, just to explore the possibilities, and to examine some of the options you have in using UWP features. The finished app is an (extremely primitive) implementation of the classic findstr tool – the app is in the Store here, and the sources are in the AppModelSamples repo on GitHub here.

To create this app from scratch, in VS, having installed the project templates, select File | New Project, and then select one of the two C++ project types for Console UWP apps – either C++/CX or C++/WinRT. In this example, I’ll choose C++/WinRT:



When prompted for the target platform version information, select a version that’s at least 17083 or later (including Windows Insider builds):



Two things to notice in the generated code. First, the Package.appxmanifest declares namespace aliases for desktop4 and iot2, and adds the SupportsMultipleInstances and Subystem attributes to both the Application node and the AppExecutionAlias node:



<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="MyFindstr.App"
desktop4:Subsystem="console"
desktop4:SupportsMultipleInstances="true"
iot2:Subsystem="console"
iot2:SupportsMultipleInstances="true"
>
...

<Extensions>
<uap5:Extension
Category="windows.appExecutionAlias"
Executable="MyFindstr.exe"
EntryPoint="MyFindstr.App">
<uap5:AppExecutionAlias
desktop4:Subsystem="console" iot2:Subsystem="console">
<uap5:ExecutionAlias Alias="MyFindstr.exe" />
</uap5:AppExecutionAlias>
</uap5:Extension>
</Extensions>

</Application>
</Applications>



From this it should be clear that in this release Console UWP apps are only supported on Desktop and IoT devices. Currently, only Desktop and IoT devices have consoles, so the feature only makes sense there.

The second thing to notice is the main function in Program.cpp. This simply gets hold of the command-line arguments and prints them out in a loop to the console.



int main()
{
// You can get parsed command-line arguments from the CRT globals.
wprintf(L"Parsed command-line arguments:n");
for (int i = 0; i < __argc; i++)
{
wprintf(L"__argv[%d] = %Sn", i, __argv);
}

wprintf(L"Press Enter to continue:");
getchar();
}



You can press F5 at this point, just to get the app built and deployed, before changing anything.



As expected, if you execute the app this way, the only command-line argument is __argv[0], which is the executable filename itself. Now that the app is deployed, you can also run it from its tile on the Start menu – and get the exact same results.

However, if you run a separate cmd (or Powershell) window, and execute the app from there, you can then provide any arbitrary command-line arguments you like. This works because installing the app also registered the AppExecutionAlias declared in the manifest. By default, this is set to the same as the project name, although you can of course change this, if you wish:



You can see from the standard generated code that you can use many of the traditional Win32 APIs – the latest Windows update adds a lot more console-specific APIs to the approved list, so that these can be used in Store-published apps.

But, what if you want to use WinRT APIs? Well, of course, this is a UWP app, so you’re free to use WinRT APIs also. This does come with a caveat, however. There are some WinRT APIs that assume that the caller is a regular UWP app with a regular window and view. A console app doesn’t have a regular window or view, so any window-specific APIs won’t work. For the more obvious window-related APIs, you should have no reason to call any of them since you don’t have your own windows, so the point should be moot.

For our simple findstr app, the first change is to add the broadFileSystemAccess restricted capability to the manifest. Add the namespace declaration at the top:



xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap mp desktop4 iot2 rescap">



…and then the capability itself at the bottom:



<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="broadFileSystemAccess" />
</Capabilities>



This corresponds to the File system setting under Privacy in the Settings app. This is where the user can enable or disable broad File-system access on either a per-app basis, or globally for all UWP apps:



Now for the meat of the app. The classic findstr tool is used to perform string searches in files. It has a lot of command-line options for configuring the search. Internally, it performs all the operations from scratch, using low-level Win32 APIs and without using any higher-level libraries. However, for the purposes of this demo, I’m going to cheat and use the std::regex APIs. One obvious reason for this is because I don’t want to write thousands of lines of code for this sample. A less obvious reason is that I want to explore how to use libraries of different types in my Console UWP app (more on this in a later post). My cut-down version of findstr will take in only two simple command-line arguments:

  • A pattern to search for.
  • A fully-qualified folder path, in which to search.

First, some housekeeping. I’ll add some #includes in my pch.h. I plan on using the Windows.Storage APIs as well as std::regex:



#include "winrt/Windows.Storage.h"
#include <regex>



At the top of my Program.cpp, I’ll add corresponding namespace declarations:



using namespace winrt;
using namespace winrt::Windows::Storage;
using namespace Windows::Foundation::Collections;



Now I can update main to get the search pattern and the starting folder supplied on the command-line. If for some reason I can’t open that folder using the Storage APIs, I’ll just bail with some suitable error code. This could be because the user has disabled File-system access in Settings, or because the folder path supplied was non-existent or badly-formed, and so on. Also note that I’m not doing any input-validation here – but of course in a real app you would be very careful to check all inputs before proceeding, and most especially if your app is expecting to have broad file-system access.



int main()
{
hstring searchPattern = to_hstring(__argv[1]);
hstring folderPath = to_hstring(__argv[2]);
StorageFolder folder = nullptr;
try
{
folder = StorageFolder::GetFolderFromPathAsync(folderPath.c_str()).get();
}
catch (...)
{
wprintf(L"Error: cannot access folder '%S'n", __argv[2]);
return 2;
}



If I did manage to get the folder open, I can then continue to use the Storage APIs to get all the files in this folder for searching, and then to call a recursive GetDirectories function, so that I can walk the folder tree from this point downwards. I’ll write the GetDirectories and SearchFile functions shortly.



if (folder != nullptr)
{
wprintf(L"nSearching folder '%s' and below for pattern '%s'n", folder.Path().c_str(), searchPattern.c_str());
try
{
IVectorView<StorageFolder> folders = folder.GetFoldersAsync().get();

// Recurse sub-directories.
GetDirectories(folders, searchPattern);

// Get the files in this folder.
IVectorView<StorageFile> files = folder.GetFilesAsync().get();
for (uint32_t i = 0; i < files.Size(); i++)
{
StorageFile file = files.GetAt(i);
SearchFile(file, searchPattern);
}
}
catch (std::exception ex)
{
wprintf(L"Error: %Sn", ex.what());
return 3;
}
catch (...)
{
wprintf(L"Error: unknownn");
return 3;
}
}

return 0;
}



Now for the recursive GetDirectories function. Given the collection of folders in the current directory, I can recursively process all sub-directories. At each level, I can also get all the files for searching.



void GetDirectories(IVectorView<StorageFolder> folders, hstring searchPattern)
{
try
{
for (uint32_t i = 0; i < folders.Size(); i++)
{
StorageFolder folder = folders.GetAt(i);

// Recurse this folder to get sub-folder info.
IVectorView<StorageFolder> subDir = folder.GetFoldersAsync().get();
if (subDir.Size() != 0)
{
GetDirectories(subDir, searchPattern);
}

// Get the files in this folder.
IVectorView<StorageFile> files = folder.GetFilesAsync().get();
for (uint32_t j = 0; j < files.Size(); j++)
{
StorageFile file = files.GetAt(j);
SearchFile(file, searchPattern);
}
}
}
catch (std::exception ex)
{
wprintf(L"Error: %Sn", ex.what());
}
catch (...)
{
wprintf(L"Error: unknownn");
}
}



Now, the SearchFile function. This is where I’m cheating and using std::regex instead of doing all the work myself. First, I’ll read all the text out of the file with ReadTextAsync. If the file isn’t a text file, this will throw an exception – for simplicity, I’m just reporting the error and moving on. Then, I enhance the raw search pattern to search for any whitespace-delimited “word” that contains the pattern. Finally, I use regex_search to do the pattern-matching, and print out all the found words and their position in the text:



void SearchFile(StorageFile file, hstring searchPattern)
{
if (file != nullptr)
{
try
{
wprintf(L"nScanning file '%s'n", file.Path().c_str());
hstring text = FileIO::ReadTextAsync(file).get();
std::string sourceText = to_string(text);
std::smatch match;
std::string compositePattern =
"(\S+\s+){0}\S*" + to_string(searchPattern) + "\S*(\s+\S+){0}";
std::regex expression(compositePattern);

while (std::regex_search(sourceText, match, expression))
{
wprintf(L"%8d %Sn", match.position(), match[0].str().c_str());
sourceText = match.suffix().str();
}
}
catch (std::exception ex)
{
wprintf(L"Error: %Sn", ex.what());
}
catch (...)
{
wprintf(L"Error: cannot read text from file.n");
}
}
}



One additional convenience: because this is a UWP app, it will have an entry in the Start menu app-list, and the user could pin its tile to Start. So, if could be executed from Start – in which case, of course, it won’t have any command-line arguments. Given how I’m assuming the command-line includes at least a search pattern and folder path, I’ll allow for this with a simple check. So, I’ll add this right at the beginning of main:



if (__argc < 3)
{
ShowUsage();
return 1;
}



…and define the matching ShowUsage function:



void ShowUsage()
{
wprintf(L"Error: insufficient arguments.n");
wprintf(L"Usage:n");
wprintf(L"MyFindstr <search-pattern> <fully-qualified-folder-path>.n");
wprintf(L"Example:n");
wprintf(L"MyFindstr on D:\Temp.n");

wprintf(L"nPress Enter to continue:");
getchar();
}



That’s it! Having re-deployed this version with F5, I can then execute it from a cmd or Powershell window, supplying a search pattern and a starting folder on the command-line:



Now admittedly, my findstr app has only the most basic functionality, but hopefully it does illustrate how easy it is to create a simple Console UWP app that consumes both standard C++ library APIs and WinRT APIs.

For anyone who has already started experimenting with Console UWP apps, it’s worth calling out that the original version of the VS templates had a couple of bugs (or, arguably, a bug and an anomaly), which were fixed in version 1.4. The issues do not prevent the app from building and executing, but they do prevent F5 debugging and also correct packaging and store publication. If you have already created projects using the old templates, you can apply the fixes by making the same changes in the .vcxproj and package.appxmanifest files manually (for both C++/CX and C++/WinRT).

First, the bug. In the package.appxmanifest file, v1.4 added iot2 to the ignorable XML namespaces:



IgnorableNamespaces="uap mp uap5 iot2 desktop4">



Of course, if you’re not targeting IoT devices, then you can instead delete this, and also delete the IoT namespace declaration itself:



xmlns:iot2="http://schemas.microsoft.com/appx/manifest/iot/windows10/2"



…as well as the iot2-scoped Subsystem declarations in both the Application and AppExecutionAlias nodes:



iot2:Subsystem="console"



Now for the anomaly: v1.3 of the template included declarations in the .vcxproj file that are specific to .NET Native. These are required if your app includes say a C# WinRT component, where .NET Native is used. However, if your app doesn’t include such a component then the declarations will prevent correct packaging. In v1.4 of the template, these have been removed – both the target framework declarations at the top of the file:



<TargetFrameworkIdentifier>.NETCore</TargetFrameworkIdentifier>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>



…and also multiple instances of the following:



<PlatformToolset>v141</PlatformToolset>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>



Please also note that all C++/WinRT templates are still experimental at this stage, and subject to change.

Apologies for any inconvenience caused by the bugs – and please do let us know if you have any feedback on the templates, or on the Console UWP feature.

For documentation, see Create a Universal Windows Platform Console app and File access permissions.

Continue reading...
 
Back
Top