[Windows] Set Platform.executable on engine start (#35560)

When setting FlutterProjectArgs.command_line_argv prior to launching the
engine, we were previously setting a placeholder value rather than the
executable name. This resulted in Platform.executable (from dart:io)
returning "placeholder" in application code.

This updates the Windows implementation for consistency with macOS and
guarantees that Platform.executable will return a reasonable value in
Dart code.

Note that this does not affect Platform.resolvedExecutable, which returns
a full, resolved path, and is implemented in the Dart runtime itself.
Previously the code:

   print(Platform.executable);
   print(Platform.resolvedExecutable);

resulted in the following output on Windows:

   flutter: placeholder
   flutter: C:\path\to\project\build\windows\runner\Debug\project.exe

after this patch, it results in:

   flutter: project.exe
   flutter: C:\path\to\project\build\windows\runner\Debug\project.exe

Issue: https://github.com/flutter/flutter/issues/83921
diff --git a/shell/platform/windows/fixtures/main.dart b/shell/platform/windows/fixtures/main.dart
index e2d76aa..57ba615 100644
--- a/shell/platform/windows/fixtures/main.dart
+++ b/shell/platform/windows/fixtures/main.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:io' as io;
 import 'dart:ui' as ui;
 
 // Signals a waiting latch in the native test.
@@ -10,6 +11,9 @@
 // Signals a waiting latch in the native test, passing a boolean value.
 void signalBoolValue(bool value) native 'SignalBoolValue';
 
+// Signals a waiting latch in the native test, passing a string value.
+void signalStringValue(String value) native 'SignalStringValue';
+
 // Signals a waiting latch in the native test, which returns a value to the fixture.
 bool signalBoolReturn() native 'SignalBoolReturn';
 
@@ -40,6 +44,11 @@
 }
 
 @pragma('vm:entry-point')
+void readPlatformExecutable() {
+  signalStringValue(io.Platform.executable);
+}
+
+@pragma('vm:entry-point')
 void drawHelloWorld() {
   ui.PlatformDispatcher.instance.onBeginFrame = (Duration duration) {
     final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(ui.ParagraphStyle())
diff --git a/shell/platform/windows/flutter_windows_engine.cc b/shell/platform/windows/flutter_windows_engine.cc
index 6c3d728..9396a9e 100644
--- a/shell/platform/windows/flutter_windows_engine.cc
+++ b/shell/platform/windows/flutter_windows_engine.cc
@@ -11,6 +11,7 @@
 #include <sstream>
 
 #include "flutter/fml/logging.h"
+#include "flutter/fml/paths.h"
 #include "flutter/fml/platform/win/wstring_conversion.h"
 #include "flutter/shell/platform/common/client_wrapper/binary_messenger_impl.h"
 #include "flutter/shell/platform/common/path_utils.h"
@@ -224,8 +225,9 @@
   // FlutterProjectArgs is expecting a full argv, so when processing it for
   // flags the first item is treated as the executable and ignored. Add a dummy
   // value so that all provided arguments are used.
+  std::string executable_name = GetExecutableName();
+  std::vector<const char*> argv = {executable_name.c_str()};
   std::vector<std::string> switches = project_->GetSwitches();
-  std::vector<const char*> argv = {"placeholder"};
   std::transform(
       switches.begin(), switches.end(), std::back_inserter(argv),
       [](const std::string& arg) -> const char* { return arg.c_str(); });
@@ -532,10 +534,9 @@
   // Convert the locale list to the locale pointer list that must be provided.
   std::vector<const FlutterLocale*> flutter_locale_list;
   flutter_locale_list.reserve(flutter_locales.size());
-  std::transform(
-      flutter_locales.begin(), flutter_locales.end(),
-      std::back_inserter(flutter_locale_list),
-      [](const auto& arg) -> const auto* { return &arg; });
+  std::transform(flutter_locales.begin(), flutter_locales.end(),
+                 std::back_inserter(flutter_locale_list),
+                 [](const auto& arg) -> const auto* { return &arg; });
   embedder_api_.UpdateLocales(engine_, flutter_locale_list.data(),
                               flutter_locale_list.size());
 }
@@ -592,4 +593,18 @@
   return node_delegate->GetNativeViewAccessible();
 }
 
+std::string FlutterWindowsEngine::GetExecutableName() const {
+  std::pair<bool, std::string> result = fml::paths::GetExecutablePath();
+  if (result.first) {
+    const std::string& executable_path = result.second;
+    size_t last_separator = executable_path.find_last_of("/\\");
+    if (last_separator == std::string::npos ||
+        last_separator == executable_path.size() - 1) {
+      return executable_path;
+    }
+    return executable_path.substr(last_separator + 1);
+  }
+  return "Flutter";
+}
+
 }  // namespace flutter
diff --git a/shell/platform/windows/flutter_windows_engine.h b/shell/platform/windows/flutter_windows_engine.h
index 3ead422..700f788 100644
--- a/shell/platform/windows/flutter_windows_engine.h
+++ b/shell/platform/windows/flutter_windows_engine.h
@@ -220,6 +220,9 @@
     root_isolate_create_callback_ = callback;
   }
 
+  // Returns the executable name for this process or "Flutter" if unknown.
+  std::string GetExecutableName() const;
+
  private:
   // Allows swapping out embedder_api_ calls in tests.
   friend class EngineModifier;
diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc
index 87315ef..1417fe4 100644
--- a/shell/platform/windows/flutter_windows_engine_unittests.cc
+++ b/shell/platform/windows/flutter_windows_engine_unittests.cc
@@ -404,5 +404,10 @@
   EXPECT_TRUE(called);
 }
 
+TEST(FlutterWindowsEngine, GetExecutableName) {
+  std::unique_ptr<FlutterWindowsEngine> engine = GetTestEngine();
+  EXPECT_EQ(engine->GetExecutableName(), "flutter_windows_unittests.exe");
+}
+
 }  // namespace testing
 }  // namespace flutter
diff --git a/shell/platform/windows/flutter_windows_unittests.cc b/shell/platform/windows/flutter_windows_unittests.cc
index 720c7b3..40cd9e3 100644
--- a/shell/platform/windows/flutter_windows_unittests.cc
+++ b/shell/platform/windows/flutter_windows_unittests.cc
@@ -118,6 +118,30 @@
   EXPECT_TRUE(bool_value);
 }
 
+// Verify that Platform.executable returns the executable name.
+TEST_F(WindowsTest, PlatformExecutable) {
+  auto& context = GetContext();
+  WindowsConfigBuilder builder(context);
+  builder.SetDartEntrypoint("readPlatformExecutable");
+
+  std::string executable_name;
+  fml::AutoResetWaitableEvent latch;
+  auto native_entry = CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
+    auto handle = Dart_GetNativeArgument(args, 0);
+    ASSERT_FALSE(Dart_IsError(handle));
+    executable_name = tonic::DartConverter<std::string>::FromDart(handle);
+    latch.Signal();
+  });
+  context.AddNativeFunction("SignalStringValue", native_entry);
+
+  ViewControllerPtr controller{builder.Run()};
+  ASSERT_NE(controller, nullptr);
+
+  // Wait until signalStringValue has been called.
+  latch.Wait();
+  EXPECT_EQ(executable_name, "flutter_windows_unittests.exe");
+}
+
 // Verify that native functions that return values can be registered and
 // resolved.
 TEST_F(WindowsTest, VerifyNativeFunctionWithReturn) {