Skip to content

Commit 4117030

Browse files
committed
Let LaunchAtLogin cover MSIX, StartupApproved and the running app on macOS
Three platform behaviours a caller had to reimplement on top of this class: - Windows, packaged apps: an MSIX container virtualizes the registry, so the Run key it writes is never read. Enable() now recognises the package and writes a shortcut to the user's Startup folder instead. - Windows: Explorer\StartupApproved\Run records that the user switched a startup entry off in Task Manager. IsEnabled() reads it, and Enable() clears it, so an entry that was switched off is not reported as enabled and enabling it again really enables it. The registry is also written through the wide API now, so a non-ASCII identifier or path survives. - macOS: SetProgram() naming the running application registers that application whatever the identifier says, instead of failing because the identifier does not name a bundled helper. Arguments are recorded but never delivered there -- SMAppService starts the app bundle, nothing else -- where before any argument made Enable() fail. tests/launch_at_login_test.cpp checks the Windows registry side: the Run value, the quoting of a path and an argument with spaces, the Task Manager flag in both directions, and a non-ASCII identifier. The examples lose their #if defined(__APPLE__) branches with it.
1 parent 028ff1e commit 4117030

7 files changed

Lines changed: 550 additions & 112 deletions

File tree

‎examples/launch_at_login_c_example/main.c‎

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,14 @@ int main(void) {
1414

1515
printf("LaunchAtLogin is supported on this platform.\n\n");
1616

17-
#if defined(__APPLE__)
1817
/*
19-
* On macOS, the default constructor registers the main app with SMAppService.
20-
* Custom identifiers are for bundled login item helpers.
18+
* Create a LaunchAtLogin manager with a custom identifier and display name. On macOS
19+
* the identifier would name a bundled login item helper, but set_program below names
20+
* this application, so the application itself is what gets registered.
2121
*/
22-
native_launch_at_login_t launch_at_login = native_launch_at_login_create();
23-
#else
24-
/* Create a LaunchAtLogin manager with a custom identifier and display name */
2522
native_launch_at_login_t launch_at_login =
2623
native_launch_at_login_create_with_id_and_display_name("com.example.myapp.c",
2724
"My C Example App");
28-
#endif
2925
if (launch_at_login == NATIVE_INVALID_LAUNCH_AT_LOGIN) {
3026
printf("Failed to create LaunchAtLogin instance.\n");
3127
return 1;
@@ -44,23 +40,20 @@ int main(void) {
4440
free_c_str(id);
4541
free_c_str(display_name);
4642

47-
#if !defined(__APPLE__)
48-
/* Set a custom program path and arguments */
43+
/*
44+
* Set a custom program path and arguments. macOS records the arguments but never
45+
* delivers them: SMAppService starts the app bundle, nothing else.
46+
*/
4947
char* argument_items[] = {"--minimized", "--launch_at_login"};
5048
native_string_list_t arguments = {argument_items, 2};
5149
native_launch_at_login_set_program(launch_at_login, executable ? executable : "", arguments);
52-
#endif
5350
free_c_str(executable);
5451

5552
/* Retrieve and display the updated executable path */
5653
char* exec_path = native_launch_at_login_get_executable_path(launch_at_login);
5754
printf("After SetProgram:\n");
5855
printf(" Executable: %s\n", exec_path ? exec_path : "(null)");
59-
#if defined(__APPLE__)
60-
printf(" Arguments: (not supported by macOS SMAppService main-app login items)\n\n");
61-
#else
6256
printf(" Arguments: --minimized --launch_at_login\n\n");
63-
#endif
6457
free_c_str(exec_path);
6558

6659
/* Check current state before enabling */

‎examples/launch_at_login_example/main.cpp‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,21 @@ int main() {
1818

1919
std::cout << "LaunchAtLogin is supported on this platform.\n\n";
2020

21-
// On macOS, the default constructor registers the main app with SMAppService.
22-
// Custom identifiers are for bundled login item helpers.
23-
#if defined(__APPLE__)
24-
LaunchAtLogin launch_at_login;
25-
#else
21+
// An identifier of your own names the registry value on Windows and the .desktop file
22+
// on Linux. On macOS it would name a bundled login item helper, but SetProgram() below
23+
// names this application, so the application itself is what gets registered.
2624
LaunchAtLogin launch_at_login("com.example.myapp", "My Example App");
27-
#endif
2825

2926
// Display current configuration
3027
std::cout << "LaunchAtLogin configuration:\n";
3128
std::cout << " ID: " << launch_at_login.GetId() << "\n";
3229
std::cout << " Display name: " << launch_at_login.GetDisplayName() << "\n";
3330
std::cout << " Executable: " << launch_at_login.GetExecutablePath() << "\n\n";
3431

35-
// macOS SMAppService main-app login items do not support arbitrary arguments.
36-
#if !defined(__APPLE__)
37-
// Set a custom program path and arguments
32+
// Set a custom program path and arguments. macOS records the arguments but never
33+
// delivers them: SMAppService starts the app bundle, nothing else.
3834
launch_at_login.SetProgram(launch_at_login.GetExecutablePath(),
3935
{"--minimized", "--launch_at_login"});
40-
#endif
4136

4237
std::cout << "After SetProgram:\n";
4338
std::cout << " Executable: " << launch_at_login.GetExecutablePath() << "\n";

‎src/launch_at_login.h‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ namespace nativeapi {
1414
* but this class abstracts away those differences.
1515
*
1616
* Platform implementations:
17-
* - Windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Run registry key
17+
* - Windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Run registry key, and the
18+
* matching Explorer\StartupApproved\Run flag so that an entry the user switched off in
19+
* Task Manager reads as disabled. An app running from an MSIX package sees a virtualized
20+
* registry instead, so there it is a shortcut in the user's Startup folder.
1821
* - macOS: ServiceManagement SMAppService for the main app or bundled login item helpers
1922
* - Linux (XDG): ~/.config/autostart/[app_id].desktop
2023
* - Android/iOS/OHOS: Not supported (returns false from IsSupported/operations)
@@ -25,8 +28,8 @@ namespace nativeapi {
2528
* - You can let the implementation determine the current executable path, or call
2629
* SetProgram() to customize the target binary and arguments recorded in the OS where the
2730
* platform supports arbitrary launch commands. On macOS, SMAppService can only register
28-
* the main app or a bundled helper, so custom executable paths and arguments are not
29-
* supported.
31+
* the main app or a bundled helper, so a custom executable path is not supported and
32+
* arguments are never delivered.
3033
* - Some platforms may require application-specific permissions or entitlements
3134
* (e.g., sandbox restrictions on macOS). In such cases, operations may fail.
3235
*
@@ -122,6 +125,16 @@ class LaunchAtLogin {
122125
* @param executable_path Absolute path to the executable to run on login.
123126
* @param arguments Optional arguments; order is preserved.
124127
* @return true if stored locally; does not change OS registration until Enable().
128+
*
129+
* @note Platform availability:
130+
* - macOS: ⚠️ Recorded only - SMAppService starts the app bundle; naming the running
131+
* application registers it whatever the identifier says, any other path makes Enable()
132+
* fail, and arguments are never delivered
133+
* - Windows: ✅ Fully supported - written as a quoted command line
134+
* - Linux: ✅ Fully supported - written to the .desktop Exec line
135+
* - Android: ❌ Not applicable - Always ignored
136+
* - iOS: ❌ Not applicable - Always ignored
137+
* - OpenHarmony: ❌ Not applicable - Always ignored
125138
*/
126139
bool SetProgram(const std::string& executable_path,
127140
const std::vector<std::string>& arguments = {});
@@ -152,6 +165,15 @@ class LaunchAtLogin {
152165
* to resolve the current executable path and use that as the program to start.
153166
*
154167
* @return true on success; false on error or when unsupported.
168+
*
169+
* @note Platform availability:
170+
* - macOS: ⚠️ Recorded only - fails when the user has denied the app in System Settings,
171+
* or when SetProgram() named an executable that is not this application
172+
* - Windows: ✅ Fully supported - also clears the Task Manager "disabled" flag
173+
* - Linux: ✅ Fully supported
174+
* - Android: ❌ Not applicable - Always returns false
175+
* - iOS: ❌ Not applicable - Always returns false
176+
* - OpenHarmony: ❌ Not applicable - Always returns false
155177
*/
156178
bool Enable();
157179

@@ -165,10 +187,14 @@ class LaunchAtLogin {
165187
/**
166188
* @brief Query whether launch-at-login is currently enabled for this manager's identifier.
167189
*
168-
* This checks the platform-specific mechanism to determine whether the app (program path
169-
* and arguments currently configured) is registered to start at user login.
190+
* This checks the platform-specific mechanism for an entry under this manager's
191+
* identifier. It does not compare the registered command against the program and
192+
* arguments currently configured: an entry written by an older version of the app, from
193+
* a different path, still reads as enabled.
170194
*
171195
* @return true if currently enabled; false otherwise.
196+
*
197+
* @see Enable() for platform availability.
172198
*/
173199
bool IsEnabled() const;
174200

‎src/platform/macos/launch_at_login_macos.mm‎

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,41 @@
8282
}
8383
}
8484

85+
// The path of the running executable as the loader knows it, which is what a caller
86+
// asking "is this me?" has most likely been handed (Dart's Platform.resolvedExecutable,
87+
// for one). It can differ from arg0, so both are compared.
88+
static std::string LoaderExecutablePath() {
89+
uint32_t size = 0;
90+
_NSGetExecutablePath(nullptr, &size);
91+
if (size == 0) {
92+
return std::string();
93+
}
94+
std::string buffer(size + 1, '\0');
95+
if (_NSGetExecutablePath(buffer.data(), &size) != 0) {
96+
return std::string();
97+
}
98+
return std::string(buffer.c_str());
99+
}
100+
85101
static bool IsCurrentProgram(const std::string& executable_path) {
86102
if (executable_path.empty()) {
87103
return true;
88104
}
89105

90-
std::string current = DetectDefaultProgramPath();
91-
if (current.empty()) {
92-
return false;
93-
}
94-
95106
@autoreleasepool {
96-
NSString* requested = [ToNSString(executable_path) stringByStandardizingPath];
97-
NSString* detected = [ToNSString(current) stringByStandardizingPath];
98-
return [requested isEqualToString:detected];
107+
NSString* requested =
108+
[[ToNSString(executable_path) stringByStandardizingPath] stringByResolvingSymlinksInPath];
109+
for (const std::string& candidate : {DetectDefaultProgramPath(), LoaderExecutablePath()}) {
110+
if (candidate.empty()) {
111+
continue;
112+
}
113+
NSString* detected =
114+
[[ToNSString(candidate) stringByStandardizingPath] stringByResolvingSymlinksInPath];
115+
if ([requested isEqualToString:detected]) {
116+
return true;
117+
}
118+
}
119+
return false;
99120
}
100121
}
101122

@@ -147,6 +168,7 @@ bool SetDisplayName(const std::string& display_name) {
147168
bool SetProgram(const std::string& executable_path, const std::vector<std::string>& arguments) {
148169
program_path_ = executable_path;
149170
arguments_ = arguments;
171+
program_was_set_ = true;
150172
return true;
151173
}
152174

@@ -226,7 +248,7 @@ bool IsEnabled() const {
226248
#if NATIVEAPI_HAS_SM_APP_SERVICE
227249
SMAppService* Service() const API_AVAILABLE(macos(13.0)) {
228250
@autoreleasepool {
229-
if (id_.empty() || id_ == default_id_) {
251+
if (UsesMainApp()) {
230252
return [SMAppService mainAppService];
231253
}
232254

@@ -239,12 +261,25 @@ bool IsEnabled() const {
239261
}
240262
#endif
241263

264+
// Whether this manager registers the application itself rather than a bundled login
265+
// item helper. An identifier that is not the bundle identifier names a helper — unless
266+
// SetProgram() named the running application, which says plainly which app to start.
267+
bool UsesMainApp() const {
268+
if (id_.empty() || id_ == default_id_) {
269+
return true;
270+
}
271+
return program_was_set_ && IsCurrentProgram(program_path_);
272+
}
273+
242274
bool CanUseConfiguredProgram() const {
243275
// SMAppService registers the main app or bundled helpers. It cannot register
244-
// an arbitrary executable path or ProgramArguments like a legacy LaunchAgent.
276+
// an arbitrary executable path like a legacy LaunchAgent, and it starts the app
277+
// bundle without arguments — those are recorded but never delivered.
278+
if (UsesMainApp()) {
279+
return program_path_.empty() || IsCurrentProgram(program_path_);
280+
}
245281
return arguments_.empty() &&
246-
(program_path_.empty() || program_path_ == default_program_path_ ||
247-
IsCurrentProgram(program_path_));
282+
(program_path_.empty() || program_path_ == default_program_path_);
248283
}
249284

250285
private:
@@ -254,6 +289,7 @@ bool CanUseConfiguredProgram() const {
254289
std::vector<std::string> arguments_;
255290
std::string default_id_;
256291
std::string default_program_path_;
292+
bool program_was_set_ = false;
257293
};
258294

259295
// LaunchAtLogin public API implementations

0 commit comments

Comments
 (0)