llfbandit / app_links

Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter.
https://pub.dev/packages/app_links
Apache License 2.0
220 stars 81 forks source link

Distinguishing url arg from other arg on windows #129

Closed twinstar6980 closed 2 months ago

twinstar6980 commented 5 months ago

On Windows, when an app is started with a URL, the system passes the URL to the app as argv[1]. However, developers may need to customize some command line parameters, see the following examples:

  1. app.exe my-app:/path?query starts the app with a URL, app_links passes the URL to linkStream, which is the expected behavior of developers.
  2. app.exe -x opt1 opt2 passes custom parameters. In this case, there is no URL, but app_links passes the argv[1] -x as the URL to linkStream, which is not the expected behavior of developers.

Therefore, please allow app_links to judge the passed command line parameters to decide whether to pass argv[1] to linkStream.

thanhle7 commented 5 months ago

From Windows terminal, you can try: app.exe "" -x opt1 opt2 In my understanding, this plugin however is based on custom Url handler using Windows default app feature. I am not sure how an empty url can help Windows recognizing and getting back to your app.

twinstar6980 commented 5 months ago

From Windows terminal, you can try: app.exe "" -x opt1 opt2 In my understanding, this plugin however is based on custom Url handler using Windows default app feature. I am not sure how an empty url can help Windows recognizing and getting back to your app.

Passing a empty string does not solve this problem, according to implementation, when argc > 1, app_links will treat argv[1] as the app url and pass it to linkStream.

My current solution is:

  1. When the app is first launched, applinks will pass argv[1] to linkStream, so I need to add logic after getting the initial link string to determine whether it is a real app url.
  2. When the app instance already exists, applinks tells us to add SendAppLinkToInstance in the wWinMain function, where I added a piece of code to judge argv[1] and only let applinks pass the url to linkStream if it is an app url.

Like this:

    AppLinks().stringLinkStream.listen((link) async {
      if (link.startsWith('my-scheme:')) {
        handleLink(Uri.parse(link));
      }
    });
// pass "my-scheme:" as pattern
bool SendAppLinkToInstance(const std::wstring& title, const std::string& pattern) {
  // check argv[1]
  std::vector<std::string> command_line_arguments = GetCommandLineArguments();
  if (command_line_arguments.size() != 1) {
    return false;
  }
  std::string& link = command_line_arguments.front();
  if (link.size() < pattern.size() || link.substr(0, pattern.size()) != pattern) {
    return false;
  }
  // ...
}
llfbandit commented 5 months ago

Indeed, on Windows you have to handle it by yourself if your app handles multiple command line parameters for now. Labeling this as an enhancement.

twinstar6980 commented 5 months ago

Indeed, on Windows you have to handle it by yourself if your app handles multiple command line parameters for now. Labeling this as an enhancement.

If possible, I would like to create a PR to solve this problem.

My solution is to add a check in the AppLinksPlugin::GetLink function: GetLink will only return the url if argc == 2 and argv[1] is an app url, otherwise it returns nullopt.

The situation that needs to be considered is how to check argv[1]. If it is simply judged whether it is a url, then if a url that does not belong to the app itself is passed, applinks will also pass it to linkStream, and developers may receive a url that does not belong to them in get*Link. For example:

  1. app.exe my-scheme:/...
  2. app.exe other-app-scheme:/...

Both urls are passed to the flutter app, but the url in the second case does not belong to my-app itself.

Should add a new api to allow users to set the allowed url scheme in wWinMain?

Do you think this solution is ok? Can I make a PR?

In addition, I also found a problem. If argc == 1, the GetLink function does not perform the LocalFree operation on the return value of CommandLineToArgvW. Will this cause a memory leak?

llfbandit commented 5 months ago

Of course, PR are always welcome!

I would avoid to introduce any new API here (also specific to Windows platform).

Could we look for registry scheme and make a match with the current process executable? In this way, new API is avoided, this is simplier for developers and also dynamic.

twinstar6980 commented 5 months ago

Of course, PR are always welcome!

I would avoid to introduce any new API here (also specific to Windows platform).

Could we look for registry scheme and make a match with the current process executable? In this way, new API is avoided, this is simplier for developers and also dynamic.

I don't really understand how Windows URL Protocol register works. I have a few questions:

  1. Searching the registry is an expensive solution compared to setting the scheme rule directly in wWinMain.
  2. Instead of adding registry entries directly, I use the protocol extension of the MSIX package to add protocol support to the app. In this case, I find HKEY_CURRENT_USER\Software\Classes\<my-scheme> in the registry, but it does not have shell/open/command keys. Is there any other way to retrieve the URL protocol more safely?

Btw, I think adding Windows-specific APIs is not unacceptable, because AppLinks already requires us to add a lot of dirty code in main.cpp. (why not add SendAppLinkToInstance to app_links_plugin_c_api? In this way, users don't need to introduce too much code in main.cpp.)

llfbandit commented 5 months ago

The idea was to put matching scheme/executable around GetLink method. So this would be fully internal.

About performance, don't worry, there's nothing to search in the registry since we already know the path: HKEY_CURRENT_USER\Software\Classes\<my-scheme>\shell\open\command. If this path does not exist, argument is not a link (or an associated one).

Pseudo positive flow from GetLink: 1- check arguments for potential link 2- get potential value from registry 3- check matching from registry value and current process (registry value should be with this form <executable_full_path> <maybe_other_params> %1.

shell\open\command should be present as subkeys, this is how Windows triggers the app launch (as far as I know).

twinstar6980 commented 5 months ago

Please see the second point of my previous comment. When we use the MSIX protocol extension, HKEY_CURRENT_USER\Software\Classes\<my-scheme> exists in the registry, but there is no shell/open/command, so it is impossible to determine whether it points to the app's exe.

24-06-04_22-12-23
twinstar6980 commented 5 months ago

For MSIX app, the actual protocol information is stored here, but I don't know how to get the AppX.... id corresponding to the exe.

24-06-04_22-16-15
llfbandit commented 5 months ago

Ok, so you register your app as an UWP app if I understand well... I didn't know that was still possible this way since Flutter removed its support.

twinstar6980 commented 5 months ago

Ok, so you register your app as an UWP app if I understand well... I didn't know that was still possible this way since Flutter removed its support.

not uwp, just win32 app, msix can packing win32 app and run it as full trust. MSIX is a more modern, cleaner and simpler app distribution solution. you can pack flutter app to msix use this dart plugin

llfbandit commented 5 months ago

Ok I may have found something with windows implementation library which should make the bridge with win32 apps.

Here's how we can experiment a solution to this internally:

In `CMakeLists.txt`, download nuget and add windows implementation library ```cmake # Windows Implementation library set(WIL_VERSION "1.0.210803.1") # need to check for updated version include(FetchContent) FetchContent_Declare(nuget URL "https://dist.nuget.org/win-x86-commandline/v6.5.0/nuget.exe" # need to check for updated version URL_HASH SHA256=d5fce5185de92b7356ea9264b997a620e35c6f6c3c061e471e0dc3a84b3d74fd DOWNLOAD_NO_EXTRACT true ) find_program(NUGET nuget) if (NOT NUGET) message(STATUS "Nuget.exe not found, trying to download or use cached version.") FetchContent_MakeAvailable(nuget) set(NUGET ${nuget_SOURCE_DIR}/nuget.exe) endif() execute_process(COMMAND ${NUGET} install Microsoft.Windows.ImplementationLibrary -Version ${WIL_VERSION} -ExcludeVersion -OutputDirectory ${CMAKE_BINARY_DIR}/packages WORKING_DIRECTORY ${CMAKE_BINARY_DIR} RESULT_VARIABLE ret) if (NOT ret EQUAL 0) message(FATAL_ERROR "Failed to install nuget package Microsoft.Windows.ImplementationLibrary.${WIL_VERSION}") endif() target_link_libraries(${PLUGIN_NAME} PRIVATE ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.ImplementationLibrary/build/native/Microsoft.Windows.ImplementationLibrary.targets) ```
In `app_links_plugin.cpp` ```cpp using namespace winrt; using namespace winrt::Windows::ApplicationModel; void RespondToActivation() { auto args = AppInstance::GetActivatedEventArgs(); auto kind = args.Kind(); if (kind == ActivationKind::Protocol) { auto protocolArgs = args.as(); if (protocolArgs) { Uri uri = protocolArgs.Uri(); // TODO Now send the link to dart via eventSink_->Success(latestLink_.value()); } } } ```

Hint: https://github.com/microsoft/WindowsAppSDK/blob/main/specs/AppLifecycle/Activation/AppLifecycle%20Activation.md#get-rich-activation-objects

Are you able to continue this research?

twinstar6980 commented 5 months ago

I tried winrt, for my MSIX packaged app, the program works as expected when I launch the app from the startmenu or protocol.

However, there is a serious problem, if I launch the app's exe from the command line, the program will crashes because of the args.Kind() statement.

twinstar6980 commented 5 months ago

In addition, when the application registers the url protocol by adding registry entry, args.Kind will also crash (I think this is because shell/open/command actually executes the bat command line).

It seems that the solution provided by winrt can only be used for msix packaged apps, and it will also cause the packaged app exe to fail to start from the command line. I have no experience with WinRT so I don't know how to avoid this crash.

llfbandit commented 3 months ago

I published version 6.2.1 which partially helps here. Now, a single argument with a protocol scheme is required. This mitigates the issue described in OP.

What I don't know is about your use case with activation from the manifest packaged by MSIX. Does it open the app the same way as with a manual registration? If so, do you use http or https scheme? Would it work?

twinstar6980 commented 3 months ago

Does it open the app the same way as with a manual registration? Yes, after the user installs MSIX, the system will automatically associate the URL with the app. However, unlike manual registration, in this case, you cannot find shell/open/command under HKCU/....../<scheme>, so you cannot determine whether the URL is associated with the exe, unless the user passes a parameter representing the URL scheme in the cpp code. WinRT can work in MSIX-wrapped apps, but for non-MSIX apps, WinRT will cause the app to crash (I am not a professional WinRT user, and I don’t know if there is any way to avoid crashes in non-MSIX).

If so, do you use http or https scheme? Would it work? I don’t know, I don’t use http/https scheme in my app.