Best method for auto launch of app after login

What is the best method to launch automatically an app after login of users. From macOS 11 and with API 2 recent version of Xojo.

If you open System Preferences and open the “Users and Groups” item. Click on your account and then the Login Items tab. You can add applications to that list and they will run when you login.

Right click on the dock icon,
Select Options/Open at login

I’m looking to do it programmatically.

You would have to create a launchDemon plist file. Permission write it outside of an installer would be tricky.

1 Like

If you use MBS Xojo Plugins, check the LSSharedFileListMBS class.
I expect it to still work for login items.

The easiest way to create a LaunchDemon is with the app Lingon (Lingon - Peter Borg Apps). It costs 10 $. Afterwards you can create the file in code.

I think that starting with Ventura LaunchDemons can come within the app. LaunchDemons are loaded and unloaded with the Terminal command launchctl. The command changed ages ago but most websites still describe the old version and not the new one.

It’s also trivial to make a Xojo app to handle all this (additionally showing the authentication dialog with the MBS plugin).

I made long time ago with the help of guys on a French forum an AppleScript which I call from Xojo with this code:

If CheckBoxLogItems.Value Then ' I put my App in the Logins items
  PosLogItemSt = AjEffLoginItems(CetAppShPath, "Le_Creer") ' Il n'existait pas, on le crée
Else ' I remove my App from the Logins items
  PosLogItemSt = AjEffLoginItems(CetAppShPath, "L_Effacer") ' Il existait, on l'efface
End If

.
With
CetAppShPath = App.ExecutableFile.Parent.Parent.Parent.NativePath
.
.
The AppleScript:

-- Même procédures dans MemoDate et LaunchOnceAday pour  AjEffLoginItems  et  LogItExists

on run {item1T, QueFaireT} -- C'est du texte, "Le_Creer" créera le Login Item si n'existe pas et "L_Effacer" l'effacera s'il existe, si vide ne fera que renvoyer si existe ou non par sa position (-1 n'existe pas) , "Test" le créera si n'existe pas et l'effacera sinon
	(* Pour Test
	tell application "Finder"
		set item1 to choose file of type "APPL" with prompt "Choose the Application :" default location (path to applications folder)
		if item1 exists then
			-- set item1Nom to displayed name of item1 as text
			set item1T to (POSIX path of item1) as text -- Note : item1 est un alias   quoted form of
			if ((characters -1 thru -1 of item1T) as text) is "/" then set item1T to (characters 1 thru -2 of item1T) as text
		end if
		set QueFaireT to "Test"
	end tell -- Finder
	activate -- Editeur AppleScript
	*)
	
	set CeLogItNoT to (AjEffLoginItems(item1T, QueFaireT) of me) as text
	
	CeLogItNoT -- A pour effet de renvoyer cette valeur à RealBasic  On est obligé de retourner du texte
end run

on AjEffLoginItems(CeShPath, QueFaire) -- ATTENTION : parfois (pour les applications notemment), il y a un '/' à la fin du path et parfois non. Ca pose problème car pas identique
	tell application "System Events"
		try
			set NbLoginItems to count of login items
		on error
			set NbLoginItems to -1
		end try
	end tell
	set CeLogItNo to LogItExists(CeShPath, NbLoginItems) of me -- , item1Nom
	-- display dialog "Ce Login Item existe au n° : " & CeLogItNo & " (-1 si n'existe pas)." & return & "Path : '" & CeShPath & "' , class : " & (class of CeShPath) & return & "QueFaire : " & QueFaire -- & return & "Nom : '" & item1Nom & "'"
	if CeLogItNo is -1 then -- Le Login Item n'existe pas
		if (QueFaire is "Le_Creer") or (QueFaire is "Test") then
			tell application "System Events"
				try
					make new login item at end of login items with properties {path:CeShPath, hidden:false} -- , kind:volume}
					set CeLogItNo to NbLoginItems + 1 -- Puisque créé en dernière position
				on error
					-- CeLogItNoreste Ă  -1
				end try
			end tell
			-- else  alors c'est "L_Effacer" et on pourrait faire une alerte pour dire que pas logique puisque n'existait pas
		end if
	else -- Le Login Item existe
		if (QueFaire is "L_Effacer") or (QueFaire is "Test") then
			tell application "System Events"
				try
					delete login item CeLogItNo
				on error
					-- CeLogItNoreste est quand mĂŞme mis Ă  -1
				end try
				set CeLogItNo to -1
			end tell
			-- else  alors c'est "Le_Creer" et on pourrait faire une alerte pour dire que pas logique puisque existait déjà
		end if
	end if
	if QueFaire is "Test" then
		tell application "System Preferences" to activate
		delay 1 -- secondes
		activate -- Finder
	end if
	return CeLogItNo
end AjEffLoginItems

on LogItExists(CeShPath, NbLogIt) -- , CeNom
	set NoLogIt to -1 -- N'existe pas
	tell application "System Events"
		-- set TsNomLogin to the name of every login item
		-- TsNomLogin
		-- set TsPathLogin to the path of every login item
		-- TsPathLogin
		-- tell application "Finder" to display dialog "Nb Log Items = " & NbLogIt as text
		repeat with iNbre from 1 to NbLogIt
			-- display dialog ("'" & (name of login item iNbre) as text) & "'" & return & "'" & ((path of login item iNbre) as text) & "'"
			if (((path of login item iNbre) as text) is CeShPath) then
				-- display dialog ("'" & (name of login item iNbre) as text) & "' existe." & return & "iNbre = " & (iNbre as text)
				set NoLogIt to iNbre
				exit repeat
			end if
		end repeat
		(* Je ne fais pas ça car si jamais il y a 2 éléments avec le même nom mais un path différent
		if login item CeNom exists then
			-- display dialog ((path of login item CeNom) as text) & return & CeShPath
			set DrapLogItExists to (((path of login item CeNom) as text) is CeShPath) as boolean
		else
			set DrapLogItExists to false
		end if *)
	end tell
	return NoLogIt
end LogItExists

(*
tell application "System Events"
	-- LOGIN ITEMS
	get the properties of every login item
	-- {{class:login item, path:"/Users/sal/Desktop/View Remote Screen.app", hidden:false, kind:"Application", name:"View Remote Screen"}}
	-- Adding a login item for the current user
	make new login item at end of login items with properties {path:"/Applications/Dictionary.app", hidden:false}
end tell
*)

Yep, this still works. I use it in a couple of applications to make launch on login available as a preference within the app itself.

I’ve been looking into the LSSharedFileListMBS class - but how do I tell where the app is, if it’s not in the Applications folder? If it’s launched from the Downloads or Desktop folder, or the user moves it, the first line of code below won’t work.

Is there a way for the app to find out where it resides?

Dim app As FolderItem = SpecialFolder.Applications.Child("MyApplication.app")

// get list object
Dim l As New LSSharedFileListMBS(LSSharedFileListMBS.kSessionLoginItems)

// insert file
Dim item As LSSharedFileListItemMBS = l.InsertFile(l.kLSSharedFileListItemBeforeFirst, "MyApplication", Nil, app)

// check error
If l.Lasterror = 0 Then
  MsgBox "OK"
Else
  MsgBox "Failed: "+Str(l.Lasterror)
End If

See app.executableFile, where you may need to use .parent on to find the app bundle.

1 Like

I missed the “.parent” part until now. In my case, it was “app.executableFile.parent.parent.parent” to get to the main app.
Terminal is no longer launching on startup, and the app is.

Is this still the preferred method of doing this?

Understanding that I could very much be wrong, I’m going to say that I thought using ServiceManagement was the official Apple “blessed” method of launching an app at login and that’s what I’ve been using in my app. Unfortunately it does not seem to work anymore in Ventura (I don’t know if it doesn’t actually work, but it’s throwing errors for me).

Using LSSharedFileList works, but always shows the notification to the user (which I understand is intentional on Apple’s part, but if a user turns my preference off and then back on again, it shows the notification again).

I’m not trying to get people going on Apple’s choices here, I just want to know what the officially “blessed” method of doing this is.

Thank you!

I have some apps in System Settings that use LSSharedFileList and there was no notification.

The officially blessed Apple way is to use SMAppServiceMBS. But only SMAppServiceMBS.openSystemSettingsLoginItems works fine.

I will continue to use the LaunchAgent written to the LaunchAgent folder with the caveat that the LaunchAgent needs a reference to the parent app. Otherwise, the app may show up with the name of the developer and not the app name.

I’m using SMAppService via declares (not a plugin) and it’s working for me.

It’s actually a really nice API, and a clear example of where charge does actually equal progress (unlike 2.0 All The Things). It is a massive improvement in terms of required code and situations to handle, when compared to the method deprecated in 10.11 or the kludgy workaround of using a LaunchAgent.

There are some things to note.

  1. It requires macOS 13.
  2. It doesn’t report an error if the application is Translocated, but it won’t work. So check this yourself.
  3. It does provide a notification to the user when the application is added.
  4. Apps get added to the Login Items list, not the background apps list.

Below is a code snippet from the action event of the OAKLaunchOnLogin checkbox class I’m using.

Dim nsError as integer
if me.value then
  if SMAppService_registerAndReturnError( mySMAppServiceApp, nserror ) = false then
    MsgBoxError "Unable to register for login", NSError_localizedDescription( nsError ), currentMethodName
  end if
  
else
  if SMAppService_unregisterAndReturnError( mySMAppServiceApp, nserror ) = false then
    MsgBoxError "Unable to unregister for login", NSError_localizedDescription( nsError ), currentmethodName
  end if
  
end if

updating = true

select case SMAppService_status( mySMAppServiceApp )
case SMAppServiceStatus.enabled
  me.value = true
  
case SMAppServiceStatus.notFound
  me.value = false
  MsgBox "Service Not found"
  
case SMAppServiceStatus.notRegistered
  // --- This fires when app is unregistered, but we only need to alert user if we're not expecting this message.
  if me.value then
    me.value = false
    msgbox "Service Not Registered"
  end if
  
case SMAppServiceStatus.requireApproval
  SMAppService_openSystemSettingsLoginItems( NSClassFromString( "SMAppService" ) )
  
else
  
  break
  
end select

updating = false
1 Like

Have you a public sample code for this ?

I no longer make my code public since a bunch of people got really angry with me for not providing a API 2.0 / DesktopControls version of my code. The translation and support of a API 1.0 version and a API 2.0 / DesktopControls version simply isn’t worth the time.

1 Like

Sigh… it’s not that complicated to switch to API 2. We can wish as much as we want that this is not necessary. I commented the code out I don’t use and converted the rest. I have started preparing for DesktopControls.

2 Likes

DesktopControls are the main problem.