diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 8b5fea520bf03..44d3c62e25fc6 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -580,8 +580,8 @@ FILE: ../../../flutter/runtime/embedder_resources.h FILE: ../../../flutter/runtime/fixtures/runtime_test.dart FILE: ../../../flutter/runtime/platform_data.cc FILE: ../../../flutter/runtime/platform_data.h -FILE: ../../../flutter/runtime/ptrace_ios.cc -FILE: ../../../flutter/runtime/ptrace_ios.h +FILE: ../../../flutter/runtime/ptrace_check.cc +FILE: ../../../flutter/runtime/ptrace_check.h FILE: ../../../flutter/runtime/runtime_controller.cc FILE: ../../../flutter/runtime/runtime_controller.h FILE: ../../../flutter/runtime/runtime_delegate.cc diff --git a/lib/ui/plugins/callback_cache.cc b/lib/ui/plugins/callback_cache.cc index 4aaa9c49cb132..719b1055ddd70 100644 --- a/lib/ui/plugins/callback_cache.cc +++ b/lib/ui/plugins/callback_cache.cc @@ -1,6 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// FLUTTER_NOLINT #include #include @@ -129,7 +130,7 @@ void DartCallbackCache::LoadCacheFromDisk() { Document d; d.Parse(cache_contents.c_str()); if (d.HasParseError() || !d.IsArray()) { - FML_LOG(WARNING) << "Could not parse callback cache, aborting restore"; + FML_LOG(INFO) << "Could not parse callback cache, aborting restore"; // TODO(bkonyi): log and bail (delete cache?) return; } diff --git a/runtime/BUILD.gn b/runtime/BUILD.gn index 58ddf06941a6d..d584693fdcfe7 100644 --- a/runtime/BUILD.gn +++ b/runtime/BUILD.gn @@ -55,8 +55,7 @@ source_set("runtime") { "embedder_resources.h", "platform_data.cc", "platform_data.h", - "ptrace_ios.cc", - "ptrace_ios.h", + "ptrace_check.h", "runtime_controller.cc", "runtime_controller.h", "runtime_delegate.cc", @@ -67,6 +66,11 @@ source_set("runtime") { "skia_concurrent_executor.h", ] + if (is_ios && flutter_runtime_mode == "debug") { + # These contain references to private APIs and this TU must only be compiled in debug runtime modes. + sources += [ "ptrace_check.cc" ] + } + public_deps = [ "//third_party/rapidjson" ] public_configs = [ "//flutter:config" ] diff --git a/runtime/dart_vm.cc b/runtime/dart_vm.cc index 1824ea709ce97..9e28d2e11f227 100644 --- a/runtime/dart_vm.cc +++ b/runtime/dart_vm.cc @@ -24,7 +24,7 @@ #include "flutter/lib/ui/dart_ui.h" #include "flutter/runtime/dart_isolate.h" #include "flutter/runtime/dart_service_isolate.h" -#include "flutter/runtime/ptrace_ios.h" +#include "flutter/runtime/ptrace_check.h" #include "third_party/dart/runtime/include/bin/dart_io_api.h" #include "third_party/skia/include/core/SkExecutor.h" #include "third_party/tonic/converter/dart_converter.h" @@ -329,7 +329,12 @@ DartVM::DartVM(std::shared_ptr vm_data, PushBackAll(&args, kDartWriteProtectCodeArgs, fml::size(kDartWriteProtectCodeArgs)); #else - EnsureDebuggedIOS(settings_); + const bool tracing_result = EnableTracingIfNecessary(settings_); + // This check should only trip if the embedding made no attempts to enable + // tracing. At this point, it is too late display user visible messages. Just + // log and die. + FML_CHECK(tracing_result) + << "Tracing not enabled before attempting to run JIT mode VM."; #if TARGET_CPU_ARM // Tell Dart in JIT mode to not use integer division on armv7 // Ideally, this would be detected at runtime by Dart. diff --git a/runtime/ptrace_ios.cc b/runtime/ptrace_check.cc similarity index 64% rename from runtime/ptrace_ios.cc rename to runtime/ptrace_check.cc index 8d4dd2182faed..56e5b15918d0a 100644 --- a/runtime/ptrace_ios.cc +++ b/runtime/ptrace_check.cc @@ -19,28 +19,41 @@ // - go/decommissioning-dbc // - go/decommissioning-dbc-engine // - go/decommissioning-dbc-tools -#include "flutter/common/settings.h" -#include "flutter/fml/build_config.h" // For OS_IOS. -#if OS_IOS && (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) +#include "flutter/runtime/ptrace_check.h" + +#if TRACING_CHECKS_NECESSARY -// These headers should only be needed in debug mode. #include #include +#include + +#include "flutter/fml/build_config.h" + +// Being extra careful and adding additional landmines that will prevent +// compilation of this TU in an incorrect runtime mode. +static_assert(OS_IOS, "This translation unit is iOS specific."); +static_assert(FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG, + "This translation unit must only be compiled in the debug " + "runtime mode as it " + "contains private API usage."); + #define PT_TRACE_ME 0 #define PT_SIGEXC 12 extern "C" int ptrace(int request, pid_t pid, caddr_t addr, int data); -static bool DebuggedIOS(const flutter::Settings& vm_settings) { +namespace flutter { + +static bool IsLaunchedByFlutterCLI(const Settings& vm_settings) { // Only the Flutter CLI passes "--enable-checked-mode". Therefore, if the flag // is present, we have been launched by "ios-deploy" via "debugserver". // // We choose this flag because it is always passed to launch debug builds. - if (vm_settings.enable_checked_mode) { - return true; - } + return vm_settings.enable_checked_mode; +} +static bool IsLaunchedByXcode() { // Use "sysctl()" to check if we're currently being debugged (e.g. by Xcode). // We could also check "getppid() != 1" (launchd), but this is more direct. const pid_t self = getpid(); @@ -48,7 +61,7 @@ static bool DebuggedIOS(const flutter::Settings& vm_settings) { auto proc = std::make_unique(); size_t proc_size = sizeof(struct kinfo_proc); - if (sysctl(mib, 4, proc.get(), &proc_size, nullptr, 0) < 0) { + if (::sysctl(mib, 4, proc.get(), &proc_size, nullptr, 0) < 0) { FML_LOG(ERROR) << "Could not execute sysctl() to get current process info: " << strerror(errno); return false; @@ -57,18 +70,16 @@ static bool DebuggedIOS(const flutter::Settings& vm_settings) { return proc->kp_proc.p_flag & P_TRACED; } -void EnsureDebuggedIOS(const flutter::Settings& vm_settings) { - if (DebuggedIOS(vm_settings)) { - return; - } - - if (ptrace(PT_TRACE_ME, 0, nullptr, 0) == -1) { +static bool EnableTracingManually(const Settings& vm_settings) { + if (::ptrace(PT_TRACE_ME, 0, nullptr, 0) == -1) { FML_LOG(ERROR) << "Could not call ptrace(PT_TRACE_ME): " << strerror(errno); // No use trying PT_SIGEXC -- it's only needed if PT_TRACE_ME succeeds. - return; + return false; } - if (ptrace(PT_SIGEXC, 0, nullptr, 0) == -1) { + + if (::ptrace(PT_SIGEXC, 0, nullptr, 0) == -1) { FML_LOG(ERROR) << "Could not call ptrace(PT_SIGEXC): " << strerror(errno); + return false; } // The previous operation causes this process to not be reaped after it @@ -78,11 +89,12 @@ void EnsureDebuggedIOS(const flutter::Settings& vm_settings) { size_t maxproc = 0; size_t maxproc_size = sizeof(size_t); const int sysctl_result = - sysctlbyname("kern.maxproc", &maxproc, &maxproc_size, nullptr, 0); + ::sysctlbyname("kern.maxproc", &maxproc, &maxproc_size, nullptr, 0); if (sysctl_result < 0) { FML_LOG(ERROR) << "Could not execute sysctl() to determine process count limit: " << strerror(errno); + return false; } const char* warning = @@ -98,6 +110,39 @@ void EnsureDebuggedIOS(const flutter::Settings& vm_settings) { { FML_LOG(ERROR) << warning; } + + return true; +} + +static bool EnableTracingIfNecessaryOnce(const Settings& vm_settings) { + if (IsLaunchedByFlutterCLI(vm_settings)) { + return true; + } + + if (IsLaunchedByXcode()) { + return true; + } + + return EnableTracingManually(vm_settings); } -#endif // OS_IOS && (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) +static TracingResult sTracingResult = TracingResult::kNotAttempted; + +bool EnableTracingIfNecessaryImpl(const Settings& vm_settings) { + static std::once_flag tracing_flag; + + std::call_once(tracing_flag, [&vm_settings]() { + sTracingResult = EnableTracingIfNecessaryOnce(vm_settings) + ? TracingResult::kEnabled + : TracingResult::kDisabled; + }); + return sTracingResult != TracingResult::kDisabled; +} + +TracingResult GetTracingResultImpl() { + return sTracingResult; +} + +} // namespace flutter + +#endif // TRACING_CHECKS_NECESSARY diff --git a/runtime/ptrace_check.h b/runtime/ptrace_check.h new file mode 100644 index 0000000000000..e3e12a1e5da5c --- /dev/null +++ b/runtime/ptrace_check.h @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_RUNTIME_PTRACE_CHECK_H_ +#define FLUTTER_RUNTIME_PTRACE_CHECK_H_ + +#include "flutter/common/settings.h" +#include "flutter/fml/build_config.h" + +namespace flutter { + +#define TRACING_CHECKS_NECESSARY \ + OS_IOS && !TARGET_OS_SIMULATOR && \ + (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) + +enum class TracingResult { + kNotAttempted, + kEnabled, + kNotNecessary = kEnabled, + kDisabled, +}; + +#if TRACING_CHECKS_NECESSARY +bool EnableTracingIfNecessaryImpl(const Settings& vm_settings); +TracingResult GetTracingResultImpl(); +#endif // TRACING_CHECKS_NECESSARY + +//------------------------------------------------------------------------------ +/// @brief Enables tracing in the process so that JIT mode VMs may be +/// launched. Explicitly enabling tracing is not required on all +/// platforms. On platforms where it is not required, calling this +/// method will return true. If tracing is required but cannot be +/// enabled, it is the responsibility of the caller to display the +/// appropriate error message to the user as subsequent attempts to +/// launch the VM in JIT mode will cause process termination. +/// +/// This method may be called multiple times and will return the +/// same result. There are no threading restrictions. +/// +/// @param[in] vm_settings The settings used to launch the VM. +/// +/// @return If tracing was enabled. +/// +inline bool EnableTracingIfNecessary(const Settings& vm_settings) { +#if TRACING_CHECKS_NECESSARY + return EnableTracingIfNecessaryImpl(vm_settings); +#else // TRACING_CHECKS_NECESSARY + return true; +#endif // TRACING_CHECKS_NECESSARY +} + +//------------------------------------------------------------------------------ +/// @brief Returns if a tracing check has been performed and its result. To +/// enable tracing, the Settings object used to launch the VM is +/// required. Components may want to display messages based on the +/// result of a previous tracing check without actually having the +/// settings object. This accessor can be used instead. +/// +/// @return The tracing result. +/// +inline TracingResult GetTracingResult() { +#if TRACING_CHECKS_NECESSARY + return GetTracingResultImpl(); +#else // TRACING_CHECKS_NECESSARY + return TracingResult::kNotNecessary; +#endif // TRACING_CHECKS_NECESSARY +} + +} // namespace flutter + +#endif // FLUTTER_RUNTIME_PTRACE_CHECK_H_ diff --git a/runtime/ptrace_ios.h b/runtime/ptrace_ios.h deleted file mode 100644 index 92a17737a8fe9..0000000000000 --- a/runtime/ptrace_ios.h +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef FLUTTER_RUNTIME_PTRACE_IOS_H_ -#define FLUTTER_RUNTIME_PTRACE_IOS_H_ - -#include "flutter/common/settings.h" - -#if OS_IOS && (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG) - -// Ensure that the current process is or was ptrace()-d at some point in its -// life. Can only be used within debug builds for iOS. -void EnsureDebuggedIOS(const flutter::Settings& vm_settings); - -#endif - -#endif // FLUTTER_RUNTIME_PTRACE_IOS_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 13394a976c92c..1d715b01d6990 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -11,6 +11,7 @@ #include "flutter/fml/message_loop.h" #include "flutter/fml/platform/darwin/platform_version.h" #include "flutter/fml/trace_event.h" +#include "flutter/runtime/ptrace_check.h" #include "flutter/shell/common/engine.h" #include "flutter/shell/common/platform_view.h" #include "flutter/shell/common/shell.h" @@ -114,6 +115,16 @@ - (instancetype)initWithName:(NSString*)labelPrefix else _dartProject.reset([project retain]); + if (!EnableTracingIfNecessary([_dartProject.get() settings])) { + NSLog( + @"Cannot create a FlutterEngine instance in debug mode without Flutter tooling or " + @"Xcode.\n\nTo launch in debug mode in iOS 14+, run flutter run from Flutter tools, run " + @"from an IDE with a Flutter IDE plugin or run the iOS project from Xcode.\nAlternatively " + @"profile and release mode apps can be launched from the home screen."); + [self release]; + return nil; + } + _pluginPublications = [NSMutableDictionary new]; _registrars = [[NSMutableDictionary alloc] init]; _platformViewsController.reset(new flutter::FlutterPlatformViewsController()); @@ -514,6 +525,7 @@ - (BOOL)createShell:(NSString*)entrypoint _threadHost.ui_thread->GetTaskRunner(), // ui _threadHost.io_thread->GetTaskRunner() // io ); + // Create the shell. This is a blocking operation. _shell = flutter::Shell::Create(std::move(task_runners), // task runners std::move(platformData), // window data diff --git a/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/shell/platform/darwin/ios/framework/Source/FlutterView.mm index 783eafa336f9b..707aa737f064c 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -40,7 +40,12 @@ - (instancetype)initWithCoder:(NSCoder*)aDecoder { } - (instancetype)initWithDelegate:(id)delegate opaque:(BOOL)opaque { - FML_DCHECK(delegate) << "Delegate must not be nil."; + if (delegate == nil) { + NSLog(@"FlutterView delegate was nil."); + [self release]; + return nil; + } + self = [super initWithFrame:CGRectNull]; if (self) { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 6114281d8695b..ba7a8b5ca9471 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -13,6 +13,7 @@ #include "flutter/fml/message_loop.h" #include "flutter/fml/platform/darwin/platform_version.h" #include "flutter/fml/platform/darwin/scoped_nsobject.h" +#include "flutter/runtime/ptrace_check.h" #include "flutter/shell/common/thread_host.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" @@ -71,7 +72,7 @@ @implementation FlutterViewController { BOOL _initialized; BOOL _viewOpaque; BOOL _engineNeedsLaunch; - NSMutableSet* _ongoingTouches; + fml::scoped_nsobject> _ongoingTouches; // This scroll view is a workaround to accomodate iOS 13 and higher. There isn't a way to get // touches on the status bar to trigger scrolling to the top of a scroll view. We place a // UIScrollView with height zero and a content offset so we can get those events. See also: @@ -102,7 +103,7 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine _engineNeedsLaunch = NO; _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); _weakFactory = std::make_unique>(self); - _ongoingTouches = [[NSMutableSet alloc] init]; + _ongoingTouches.reset([[NSMutableSet alloc] init]); [self performCommonViewControllerInitialization]; [engine setViewController:self]; @@ -145,7 +146,7 @@ - (instancetype)initWithCoder:(NSCoder*)aDecoder { - (void)awakeFromNib { [super awakeFromNib]; - if (!_engine.get()) { + if (!_engine) { [self sharedSetupWithProject:nil initialRoute:nil]; } } @@ -156,15 +157,22 @@ - (instancetype)init { - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project initialRoute:(nullable NSString*)initialRoute { + auto engine = fml::scoped_nsobject{[[FlutterEngine alloc] + initWithName:@"io.flutter" + project:project + allowHeadlessExecution:self.engineAllowHeadlessExecution]}; + + if (!engine) { + return; + } + _viewOpaque = YES; _weakFactory = std::make_unique>(self); - _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" - project:project - allowHeadlessExecution:self.engineAllowHeadlessExecution]); + _engine = std::move(engine); _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute]; _engineNeedsLaunch = YES; - _ongoingTouches = [[NSMutableSet alloc] init]; + _ongoingTouches.reset([[NSMutableSet alloc] init]); [self loadDefaultSplashScreenView]; [self performCommonViewControllerInitialization]; } @@ -310,8 +318,38 @@ - (void)pushRoute:(NSString*)route { #pragma mark - Loading the view +static UIView* GetViewOrPlaceholder(UIView* existing_view) { + if (existing_view) { + return existing_view; + } + + auto placeholder = [[[UIView alloc] init] autorelease]; + + placeholder.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + placeholder.backgroundColor = UIColor.whiteColor; + placeholder.autoresizesSubviews = YES; + + // Only add the label when we know we have failed to enable tracing (and it was necessary). + // Otherwise, a spurious warning will be shown in cases where an engine cannot be initialized for + // other reasons. + if (flutter::GetTracingResult() == flutter::TracingResult::kDisabled) { + auto messageLabel = [[[UILabel alloc] init] autorelease]; + messageLabel.numberOfLines = 0u; + messageLabel.textAlignment = NSTextAlignmentCenter; + messageLabel.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + messageLabel.text = + @"In iOS 14+, Flutter application in debug mode can only be launched from Flutter tooling, " + @"IDEs with Flutter plugins or from Xcode.\n\nAlternatively, build in profile or release " + @"modes to enable re-launching from the home screen."; + [placeholder addSubview:messageLabel]; + } + + return placeholder; +} + - (void)loadView { - self.view = _flutterView.get(); + self.view = GetViewOrPlaceholder(_flutterView.get()); self.view.multipleTouchEnabled = YES; self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; @@ -346,6 +384,9 @@ static void sendFakeTouchEvent(FlutterEngine* engine, } - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView { + if (!_engine) { + return NO; + } CGPoint statusBarPoint = CGPointZero; sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown); sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kUp); @@ -414,6 +455,10 @@ - (void)removeSplashScreenView:(dispatch_block_t _Nullable)onComplete { } - (void)installFirstFrameCallback { + if (!_engine) { + return; + } + fml::WeakPtr weakPlatformView = [_engine.get() platformView]; if (!weakPlatformView) { return; @@ -516,6 +561,10 @@ - (void)setFlutterViewDidRenderCallback:(void (^)(void))callback { #pragma mark - Surface creation and teardown updates - (void)surfaceUpdated:(BOOL)appeared { + if (!_engine) { + return; + } + // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and raster // thread. if (appeared) { @@ -536,14 +585,12 @@ - (void)surfaceUpdated:(BOOL)appeared { - (void)viewDidLoad { TRACE_EVENT0("flutter", "viewDidLoad"); - if (_engineNeedsLaunch) { + if (_engine && _engineNeedsLaunch) { [_engine.get() launchEngine:nil libraryURI:nil]; [_engine.get() setViewController:self]; _engineNeedsLaunch = NO; } - FML_DCHECK([_engine.get() viewController] == self) - << "FlutterViewController's view is loaded but is not attached to a FlutterEngine"; [_engine.get() attachView]; [super viewDidLoad]; @@ -594,12 +641,12 @@ - (void)viewDidDisappear:(BOOL)animated { } - (void)flushOngoingTouches { - if (_ongoingTouches.count > 0) { - auto packet = std::make_unique(_ongoingTouches.count); + if (_engine && _ongoingTouches.get().count > 0) { + auto packet = std::make_unique(_ongoingTouches.get().count); size_t pointer_index = 0; // If the view controller is going away, we want to flush cancel all the ongoing // touches to the framework so nothing gets orphaned. - for (NSNumber* device in _ongoingTouches) { + for (NSNumber* device in _ongoingTouches.get()) { // Create fake PointerData to balance out each previously started one for the framework. flutter::PointerData pointer_data; pointer_data.Clear(); @@ -633,7 +680,6 @@ - (void)dealloc { object:self userInfo:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self]; - [_ongoingTouches release]; [super dealloc]; } @@ -719,6 +765,10 @@ - (void)goToApplicationLifecycle:(nonnull NSString*)state { // touch is specified in the second argument. - (void)dispatchTouches:(NSSet*)touches pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change { + if (!_engine) { + return; + } + const CGFloat scale = [UIScreen mainScreen].scale; auto packet = std::make_unique(touches.count); @@ -887,7 +937,7 @@ - (void)viewDidLayoutSubviews { // This must run after updateViewportMetrics so that the surface creation tasks are queued after // the viewport metrics update tasks. - if (firstViewBoundsUpdate && applicationIsActive) { + if (firstViewBoundsUpdate && applicationIsActive && _engine) { [self surfaceUpdated:YES]; flutter::Shell& shell = [_engine.get() shell]; @@ -1040,6 +1090,9 @@ - (NSUInteger)supportedInterfaceOrientations { #pragma mark - Accessibility - (void)onAccessibilityStatusChanged:(NSNotification*)notification { + if (!_engine) { + return; + } auto platformView = [_engine.get() platformView]; int32_t flags = 0; if (UIAccessibilityIsInvertColorsEnabled())