DEV Community 👩‍💻👨‍💻

Anes Abismail
Anes Abismail

Posted on • Originally published at anesabml.github.io

Autostart scrcpy on device connection.

OnCreateBlog:

When it comes to programming and setting up my workflow, I always try to find a solution to automate basic tasks and focus on what matters, also it gives me a sense of magic. One of the things that were bugging me lately is manually launching scrcpy every time I plug my phone to test my apps, in this blog post I will show you how I automated that process, note that this setup is for macOS.

There's a command-line tool allows to execute a command whenever a new device is connected to adb, but that didn't work for me, you can check out here.

My thought process:

As for each problem a plan is required, my initial thought was to set up some kind of Cron job that runs every second and check for new USB devices, but this is brute force and it's not efficient to run a job every second, even if we set the job to run every minute that would not be a good idea either if I plug my phone I might have to wait one minute for the job to launch scrcpy.

Okay what other options we have, let's run a job and write an infinite loop that checks for new USB devices. this solves the instant problem but it's not efficient.

I had one last thought and it's to set up some sort of a listener, callback on an event that gets triggered every time a USB device is plugged in. After doing some research I found out that it is possible thanks to launchd

What is launchd:

launchd is a service management tool used by macOS it was created by Apple, it's similar in some ways to systemd on Linux.

Another definition according to Apple:

The launchd process is used by macOS to manage daemons and agents, and you can use it to run your shell scripts.

But wait what are daemons and agents?

A Daemon is a computer program that runs on background without any direct control and interaction with the user, they are usually launched when the system boots. An example of a daemon is a backup schedule when you enable Time Machine.

An Agent is very familiar to a Daemon but it runs within the user session, which means that they are launched when the user logs in.

Based on the Apple documentation we can find the various daemons and agents managed by launchd by looking inside the following folders:

Tables Are
/System/Library/LaunchDaemons Apple-supplied system daemons
/System/Library/LaunchAgents Apple-supplied agents that apply to all users on a per-user basis
/Library/LaunchDaemons Third-party system daemons
/Library/LaunchAgents Third-party agents that apply to all users on a per-user basis
~/Library/LaunchAgents Third-party agents that apply only to the logged-in user

Creating a LaunchDaemon or LaunchAgent:

Based on those definitions, we will proceed with creating an Agent because it's more suitable to solve our problem, we don't want to launch scrcpy if the user is not logged in.

To create a new LaunchAgent we need to add our config file to the respective directory, I will be adding my config file to ~/Library/LaunchAgents.

Those files require a specific format: plist (property list) it holds the specific information of the task. Below are the required keys to include in the file:

  • Label: Unique task identifier.
  • Program or ProgramArguments: String Path to executable.
  • RunAtLoad: (True or False) Run job immediately when plist file is loaded.
  • LaunchOnlyOnce: Run job once, and never try again.
  • LaunchEvents: there is more than one trigger or launch event but the one that we are going to use is: I/O kit notifications - USB device.

Note that the difference between Program and ProgramArguments is that when using ProgramArguments you can specify the path to executable and an array of arguments arg[0],arg[1],....

So hers is my config file, which I name com.oneplus5t.scrcpy.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN <http://www.apple.com/DTDs/PropertyList-1.0.dtd> >
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.oneplus5t.scrcpy</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/launch_iterm.bash</string>
        <string>scrcpy</string>
    </array>
    <key>LaunchEvents</key>
    <dict>
            <key>com.apple.iokit.matching</key>
            <dict>
                    <key>com.apple.device-attach</key>
                    <dict>
                            <key>idProduct</key>
                            <integer>1234</integer>
                            <key>idVendor</key>
                            <integer>1444</integer>
                            <key>IOProviderClass</key>
                            <string>IOUSBDevice</string>
                            <key>IOMatchLaunchStream</key>
                            <true/>
                    </dict>
            </dict>
    </dict>
</dict>
</plist>

To find your phone's idProduct and idVendor run this command:

ioreg -p IOUSB -l

Explaining what the above command does:

  1. ioreg displays the I/O Kit registry.
  2. -p option is to traverse IOUSB registry
  3. -l to show more information about the displayed objects.

You may notice that we are running a script launch_iterm.bash, thanks to this script we can launch iTerm2 or any other terminal application, it uses AppleScript which is a scripting language created by (wait for it) → Apple. check out this.

#!/bin/bash
#
# Open new iTerm window from the command line using v3 syntax for `AppleScript` as needed in iTerm2 Version 3+
# This script blocks until the cmd is executed in the new iTerm2 window.  It then leaves the window open.
#
# See also <https://www.iterm2.com/documentation-scripting.html>
#
# Usage:
#     iterm                   Opens the current directory in a new iTerm window
#     iterm [PATH]            Open PATH in a new iTerm window
#     iterm [CMD]             Open a new iTerm window and execute CMD
#     iterm [PATH] [CMD] ...  You can probably guess
#
# Example:
#     iterm ~/Code/HelloWorld ./setup.sh
#
# References:
#     iTerm AppleScript Examples:
#     <https://gitlab.com/gnachman/iterm2/wikis/Applescript>
#
# Credit:
#     Forked from <https://gist.github.com/vyder/96891b93f515cb4ac559e9132e1c9086>
#     Inspired by tab.bash by @bobthecow
#     link: <https://gist.github.com/bobthecow/757788>

# OSX only
[ `uname -s` != "Darwin" ] && echo 'OS X Only' &&return

function iterm () {
    local cmd=""
    local wd="$PWD"
    local args="$@"

    if [ -d "$1" ]; then
        wd=$(cd "$1"; pwd)
        args="${@:2}"
    fi

    if [ -n "$args" ]; then
        # echo $args
        cmd="$args"
    fi

osascript <<EOF
    tell application "iTerm"
        activate
        set new_window to (create window with default profile)
        set cSession to current session of new_window
        tell new_window
            tell cSession
                delay 1
                write text "$cmd"
            end tell
        end tell
    end tell
EOF
}
iterm $@

One thing left to do is to load the Agent and plug your phone, to load the Agent we use the launch tool:

launchctl load com.oneplus5t.scrcpy.plist

After loading the configuration file, you will notice that it's repeatedly executed each 10s, as I understand this is caused because the scrcpy doesn't return a result and this causes the executable being called repeatedly (if I am mistaken please correct me). Thanks to this repo, our problem can be fixed:

  1. Unload the configuration file

    launchctl unload com.oneplus5t.scrcpy.plist
    
  2. Clone the repo:

    git clone <https://github.com/himbeles/mac-device-connect-daemon>
    
  3. Build the event stream handler:

    gcc -framework Foundation -o xpc_set_event_stream_handler xpc_set_event_stream_handler.m
    
  4. Copy the result to /usr/local/bin/

    cp xpc_set_event_stream_handler /usr/local/bin/
    
  5. Update the LaunchAgent configuration file:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC -//Apple Computer//DTD PLIST 1.0//EN <http://www.apple.com/DTDs/PropertyList-1.0.dtd> >
    <plist version="1.0">
    <dict>
        <key>Label</key>
        <string>com.oneplus5t.scrcpy</string>
        <key>ProgramArguments</key>
        <array>
            <string>/usr/local/bin/xpc_set_event_stream_handler</string> <!-- Add this line -->
            <string>/usr/local/bin/launch_iterm.bash</string>
            <string>scrcpy</string>
        </array>
        <key>LaunchEvents</key>
        <dict>
                <key>com.apple.iokit.matching</key>
                <dict>
                        <key>com.apple.device-attach</key>
                        <dict>
                                <key>idProduct</key>
                                <integer>1234</integer>
                                <key>idVendor</key>
                                <integer>1444</integer>
                                <key>IOProviderClass</key>
                                <string>IOUSBDevice</string>
                                <key>IOMatchLaunchStream</key>
                                <true/>
                        </dict>
                </dict>
        </dict>
    </dict>
    </plist>
    

OnDestroyBlog:

This was a fun experiment, it opens a wide variety of automation ideas and improving my workflow, Thank you for reading I hope you learned something new and you had fun as I did, Feel free to reach out on Twitter if you have any questions.

Resources:

Launchd - At Your Service!

macOS: Know the difference between launch agents and daemons, and use them to automate processes

Daemons and Agents

Script management with launchd in Terminal on Mac

Mac device connect daemon

Bash function for opening up iterm form current directory

Top comments (0)

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.