MacOS – Why is networksetup so slow compared to manually changing Wi-Fi networks

applescriptmacosNetworkterminalwifi

if I run
networksetup -setairportnetwork en0 myssid

it takes between 10 and 15 seconds.

If I manually click the Wi-Fi icon in the notification bar and select myssid, it's instant.

Alternatively, I'd love to be able to make an applescript that manually clicks to change Wi-Fi networks, but I can't find a good example, everyone just says "use networksetup".

Answer:
@CJK's solution works great, I'm using his second script that does the manual click. The first one works in most cases except where I'm trying to connect to an enterprise network that uses a certificate. For some reason connecting this way doesn't work in that case. But in all other cases both scripts work great.

Best Answer

I note what @Allan has stated in his comment below your question.

Whilst I don't get a delay as long as 10-15 seconds using the commandline, I do find it demonstrably and consistently slower (by a few seconds) than clicking the WiFi icon. I don't know the underlying operations performed by networksetup, but I'm wondering if it performs a network scan each and every time a request is made to join a new network, which may account for the delay. Network scanning times can be influenced by many factors, so this delay would likely vary between users in different environments. This is just hypothesis, though.

I'd love to be able to make an applescript that manually clicks to change Wi-Fi networks

I'll give you two scripts.

1. Service-scripting with AppleScriptObjC

The first script uses AppleScriptObjC to interface with the system directly and control the WiFi interface. A quick summary, which I expand on below:

  • + Robust/reliable
  • + Faster than networksetup
  • + No need to manually enter passwords
  • + No visual disturbances on screen; operates in background
  • Slower than clicking the WiFi icon
  • SSID case-insensitive, whole phrase matching (can be changed)
use framework "CoreWLAN"

property this : a reference to current application
property nil : a reference to missing value
property _1 : a reference to reference

property CWWiFiClient : a reference to CWWiFiClient of this
property NSPredicate : a reference to NSPredicate of this


to joinNetwork given name:ssid as text, password:pw as text : missing value
    local ssid, pw

    set |?| to NSPredicate's predicateWithFormat:("self.ssid ==[c] %@") ¬
        argumentArray:{ssid}

    tell CWWiFiClient's sharedWiFiClient()'s interface()
        its setPower:true |error|:nil

        set networks to {}
        tell cachedScanResults() to if it ≠ missing value then ¬
            set networks to filteredSetUsingPredicate_(|?|)

        if the number of networks = 0 then set networks to ¬
            (its scanForNetworksWithName:ssid |error|:nil)

        set network to (allObjects() in networks)'s firstObject()
        its associateToNetwork:network |password|:pw |error|:_1

        set [success, E] to the result
        if E ≠ missing value then return ¬
            E's localizedDescription() ¬
            as text
        success
    end tell
end joinNetwork

You would then call the joinNetwork handler from within your script like so:

joinNetwork given name:"my network ssid", password:"Passw0rd1"

This script will be robust and reliable (barring any human error on my part, which I will correct if you find a bug I've overlooked during testing). It preferentially uses cached scan results to connect to networks; if none are available, or the specified SSID didn't appear when the scan results were last cached, it will perform a fresh network scan. Therefore, if my hypothesis about networksetup is a contributing factor, this script will be faster than the commandline on the majority of runs, but may take the same amount of time on the occasional run when forced to do a scan.

One benefit is that, because you can specify a network password when calling the joinNetwork handler, you won't need to enter it manually when joining a new network.

The SSID—specified by the name parameter—is case-insensitive in both scripts; the password, obviously, is not. The script above has elected to perform an entire phrase match on the SSID, i.e. "my net" will not ever join a network called "my network"; the script below has elected to do a fuzzy matching, i.e. "net" will potentially join "my net" or "my network".

2. UI Scripting

This second script is actually the one you requested: it interfaces with the UI to click the WiFi menu icon and then select the network (if it appears in the list). A summary:

  • + Despite being a UI script, it ought not to suffer from the same weaknesses inherent to most UI scripts; so hopefully reliable
  • + Fastest implementation—networks joined immediately
  • Visually jarring when menu appears; also, user must not interact with system until the script has exited (the nature of any UI script)
  • Must manually enter passwords if not already saved
  • SSID case-insensitive, fuzzy matching (can be changed)
    use application "System Events"

    property process : a reference to application process "SystemUIServer"
    property menu bar : a reference to menu bar 1 of my process
    property menu bar item : a reference to (menu bar items of my menu bar ¬
        where the description contains "Wi-Fi")
    property menu : a reference to menu 1 of my menu bar item
    property menu item : a reference to menu items of my menu


    to joinNetwork given name:ssid as text
        local ssid

        if not (my menu bar item exists) then return false
        click my menu bar item

        repeat until my menu exists
            delay 0.5
        end repeat

        set M to a reference to (my menu item where the name contains ssid)

        repeat 20 times --> 10 seconds @ 0.5s delay
            if M exists then exit repeat
            delay 0.5
        end repeat
        click M
    end joinNetwork


    joinNetwork given name:"my network ssid"

Scripts that try to control the UI are using a method called UI or GUI scripting. These are popular amongst and popularised by novice scripters, simply because these are the most commonly-found scripts on the internet. I won't go into it here too much other than to caution you against using scripts that rely on clicks and keystrokes—they're inherently fragile and unreliable even when well written, but most UI scripts (written by novices) are terribly written, which compounds the problem. There's almost always a better way to achieve the desired outcome.

That said, this is one of the few sorts of UI scripts that are perhaps an exception to my warning, and in my view, a pretty acceptable implementation. This is because it's targeting an aspect of the UI (a menu bar icon owned by the system) that is virtually guaranteed to always be accessible regardless of what window or desktop has focus, etc. The one situation where the script of this nature would potentially break is if the menu bar icon wasn't there (i.e. the user has specifically set a preference not to have the WiFi icon available); however, I've put a line in the script that checks for this, and performs a graceful exit in that situation.

This script appears to be the speediest implementation, which doesn't surprise me as it will be equal in speed to clicking the icon manually; but, it impresses me in that I can't think of many UI scripts that would end up out-performing an application- or service-targetted script. The network is joined almost instantly, provided the password is already saved in your keychain. On occasion, the network you'd expect to see in the menu list might not be there until the background scanning refreshes and the list updates. Therefore, I built in a 10-second period within which the script checks every half second to see if the menu item has appeared; if it doesn't, the script exits and you'll have to try again.

The drawback is the absence of a keychain password for a network will necessitate you needing to manually enter the password when asked for it. It is possible to script this using UI techniques as well, but this immediately makes the script highly susceptible to the issues I mentioned and then I, personally, wouldn't use it.


System info: AppleScript version: 2.7 System version: 10.13.6