How to make Mac OS X friends with Microsoft DFS

    Although this is ABBYY's corporate blog, in this article I will not touch on our products and our company, but on the solution to one practical issue: access to network resources from Mac OS X in a situation where the company uses Microsoft DFS (Distributed File System). In large companies, this technology is used at every step, and ABBYY is no exception. Indeed, instead of a disparate system of servers with resources scattered around them, the user sees a logically arranged tree of network resources.

    When, five years ago, I decided to change my work laptop to MacBook Pro, the team was met with mixed feelings. This was especially skeptical in the team of system administrators. Although I led this team over the past five years, I was supposed to have very slight indulgences, and the use of Mac OS, from the point of view of our admins, went beyond just indulgences. But I said that I would take all the problems upon myself and almost kept my word, although I admit that sometimes I still had to annoy the admins with my Makov problems. But I managed to solve the problem with DFS myself, because it really bothered me.



    The problem of access to DFS has been discussed in the forums for a long time, all interested persons have been waiting for normal support of this technology from Apple for several years. But, unfortunately, things are still there (in Cupertino, apparently).

    I’ll briefly outline the problem: I regularly receive letters with links in UNC format to files, something like this:

    \\dfs\Common\Media\Pictures\New Year Party\Aram on his head.jpg

    The latest versions of Outlook started automatically inserting a URL link into the letters that looked something like this:

    file:////dfs/Common/Media/Pictures/New%20Year%20Party/…

    For me, this is a bear service, since Mac OS does not know what protocol to use at this link. If you are lucky, Outlook didn’t set the link itself, then Mail.app in Snow Leopard can automatically convert UNC paths to a viewsmb://server/share/path. It’s a good function, but for it to help, you need to be very lucky: the source path should not be to the DFS resource and the link should not be embedded in the HTML text. In addition, the enthusiasm of Mail.app breaks off at the first space encountered, therefore, very often such automatic links are also broken. But even opening the “Connect to Server” window in Finder, copying the source text with handles, correcting all backslashes to direct, everything will work out only if the source path is the real path to the real computer, and not the DFS path.

    I must admit that there is a global solution to this problem, called ADmitMac, but it costs a lot of money and does a lot of other things, which is more harmful than useful because this program goes too deep into the system.

    The second, simpler solution, called Diffiss , is similar to the experimental work of a novice programmer. The product allows you to navigate through the DFS tree, however, until the main application window is closed, which after that cannot be opened except by restarting the application. This program allows you not to keep in mind the correspondence of DFS nodes to real servers, and to walk through the DFS tree, but it does not help to open links from letters. Handles have to walk on a tree, first inside Diffiss, then inside Finder. But for a while even this was a relief.

    Recently, I almost accidentally stumbled upon this article, which made me take up a thorough solution to the problem, that is, make solid crutches, in order to stumble to the time when, finally, Apple will support DFS in the system itself at the Finder level. By the way, in Samba 3.0.28, which comes with OS X Snow Leopard, work with DFS is supported (which I actually used).

    Crutches consist of two parts. One is a bash script that receives the name of the DFS root server and the source path as input, and outputs the UNC path to the directory on the real server. The second part is a small AppleScript script that gets the user’s source path, runs a bash script, and opens the desired directory in Finder.

    Consider each of the details separately.

    #! / bin / bash
     
    # This script is designed to convert the DFS path to the resource 
    # to the path to the real shared resource on the Windows network
     
    # The script expects the input:
    # arg1: the name of the root DFS server 
    # arg2: the UNC path to be converted to real path
    # The script assumes that user authentication is done through Kerberos. 
     
    if [-z "$ 1"] || [-z "$ 2"]; then
        echo -e "Error: the script has two arguments."
        echo 
        exit 100
    fi
     
    DFS_SERVER = $ 1
     
    # Convert backslashes to direct, delete possible spaces at the beginning of 
    # lines and translate all letters to lowercase
    DFS_PATH = $ (echo -n $ 2 | sed 's / \\ / \ // g' | sed 's / ^ * // g' | tr "[: upper:]" "[: lower:]")
    real_share = ""
    exit_code = "0"
     
    # Get a list of DFS nodes using rpcclient (see the expression at the end of the loop, 
    # immediately after 'done'). This method of invoking the command was done in order to avoid 
    # using channels, because for implementing channels, the loop is executed in a subshell 
    # that masks the variables that we want to modify in the loop.
     
    while read smb_path; do
        read comment
        read state
        read num_stores
        # Separate fields from labels, remove spaces at the beginning, and translate letters to lowercase 
        smb_path = $ (echo $ smb_path | awk -F: ' {gsub (/ ^ [\ t] + /, "", $ 2); print $ 2} '| \
            tr "[: upper:]" "[: lower:]")
        num_stores = $ (echo $ num_stores | awk -F: '{gsub (/ ^ [\ t] + /, "", $ 2); print $ 2} ')
        state = $ (echo $ state | awk -F:' {gsub (/ ^ [\ t] + /, "", $ 2); print $ 2} ')
     
        # Check that digits are entered in the num_stores and state variables and 
        # that they are not empty. If not, then apparently the
        expr "$ num_stores + $ state"> / dev / null 2> / dev / null
     
        if [$? ! = 0] || [-z "$ num_stores"] || [-z "$ state"]; then
            echo -e "Error trying to get a list of DFS hosts."
            exit_code = "200"
            break
        fi
     
        # Number of real resources,
        # In such cases, we will use the smbclient call to select a resource.
        for ((store = 0; store <$ num_stores; store ++)); do
            read server
            read share
        done
     
        # Check if the beginning of the DFS path matches any of the resources 
        # from the received list
        if ["$ state" -eq "1"] && [["$ DFS_PATH" == "$ smb_path" *]]; then
            if ["$ num_stores" -gt "1"]; then
                # If the number of resources per node is more than one, call smbclient. 
                # The output consists of two lines, we are interested in the second
                while read buff; do
                    if [! -z "$ buff"] && [[" 
                        real_share = "$ buff"; 
                    else 
                        # If the second line
                        # does not look  like a UNC path to the resource, it means an error
                        echo -e "Error while calling smbclient: $ buff"
                        exit_code = "300"
                    fi
                done <<(smbclient -k -c showconnect $ smb_path 2 > / dev / null)
            else
                server = $ (echo $ server | awk -F: '{gsub (/ ^ [\ t] + /, "", $ 2); print $ 2}')
                share = $ (echo $ share | awk -F: '{gsub (/ ^ [\ t] + /, "", $ 2); print $ 2}')
                real_share = "// $ server / $ share"
            fi
            # Add to the real server and resource the rest of the UNC path
            if [! -z "$ real_share"]; then
                smb_path_len = "$ {# smb_path}"
                rest_of_path = $ (awk -v len = "$ smb_path_len" -v awk_string = "$ DFS_PATH" \
                    'BEGIN {print substr (awk_string, len + 1)}')
                real_share = " $ real_share $ rest_of_path "
            fi
            # We search until the first match, so we leave the loop
            break
        fi
    done <<(rpcclient -k --command =" dfsenum 3 "$ DFS_SERVER | sed 's / \\ / \ // g')
     
    # If no value was assigned to real_share (most likely because 

    # return the original path
    if [-z "$ real_share"]; then
        real_share = $ DFS_PATH
    fi
     
    # To return the result to AppleScript, we need to output it 
    # to the standard output stream
    echo $ real_share
     
    exit $ exit_code
     


    The script, of course, is not perfect, everything can certainly be done better. In general, I rarely write scripts; Mac OS users usually do not need them, so I haven’t filled my hand yet. The technical details are clear from the comments, but essentially it does the following:
    • using the rpcclient program, which is part of the Samba package, we find out the node table from the DFS root server, linked to real servers. It should be noted here that the script assumes authentication through Kerberos, which most likely means that your computer is listed in the domain and you are already logged in with the domain name and password. Without this, living in a Windows server environment is very tight.
    • then we analyze the resulting table (the above article describes the structure of rpcclient output, so I won’t repeat it here). As the nodes are parsed, we look for one that matches the beginning of our original path. As soon as we find, we stop further searches, make up the real path from the parts and display it with the echo command. This is the standard way to transfer work results from the shell to AppleScript.


    There is one peculiarity in this whole story. The fact is that DFS contains built-in duplication mechanisms that allow you to store mirrored copies of data on different servers. Therefore, several real resources on different servers can correspond to one node. It would be easy to take one of them (if nothing is changed in the code, then the last one will be taken), but I decided to use the choice that Samba would make, for which, in such cases, smbclient is run with a single command that returns the path to the resource on the server to which you want to connect. By the way, I suspect that Samba selects nodes at random (the code did not look), but oh well.

    Now that we have figured out the first script, let's move on to the second. It is written so that it can be run both from the command line (using osascript) and interactively. In fact, there is a third way to start, about it just below.

    - This script connects to the Finder a Windows network resource via the DFS path to the resource. 
    - The script calls an external bash script to convert the DFS path to the path 
    to the real shared resource
     
    property DFSRootServer: "dfs.mycompany" - here we insert the name of the DFS root server
    property BashScriptPath: "/ Users / Shared / Scripts / dfs_to_share" - the path to our bash script
    property isRunFromCommandLine: true
     
    - This handler is used by the ThisService program to organize 
    - call the script from the Services menu. 
    - Also we call it from the "run" method. The original UNC path is passed as an argument.
    on process (input)
        try
            set realPath to do shell script "/ bin / bash" & BashScriptPath & ¬
                "" & DFSRootServer & "'" & input & "'"
        on error errMessage number errNumber
            - Our script returns different error codes in different situations, we process it here
            if errNumber is 100 then
                set errMessage to "Internal error executing script."
            else if errNumber is 200 then
                set errMessage to "Cannot get the DFS table of nodes from the server. ¬
                    Check if you have an active Kerberos ticket."
            else if errNumber is 300 then
                set errMessage to "

                set errMessage to "An unknown error occurred while executing a shell script:" & (errNumber as text)
            end if
            if isRunFromCommandLine then
                - If the application is running from the command line, 
                display a message in the console
                log errMessage
            else
                display dialog errMessage buttons {" Close "}
            end if
            return
        end try
        try
            (* The most convenient way to force Finder to open the directory in the specified path is to 
                use the" open location "method. It automatically mounts the device and 
                opens the necessary directory in the Finder window. But this method requires an input 
                path in the URL view, so we use python to convert the 
                UNC path to the URL view *)
            set urlOfPath to "smb:" & (do shell script ¬
                "python -c 'import urllib, sys; print urllib .pathname2url (sys.argv [1]) '"& ¬
                quoted form of realPath)
            tell application" Finder "
                open location urlOfPath
            end tell
        on error errMessage
            display dialog errMessage buttons {CloseButton}
        end try
    end process
     
    on run argv
        set ConnectButton to" To plug"
        set CancelButton to "Cancel"
        - Check if we were run from the command line with the path as the parameter
        if (count of argv) is equal to 0 then
            display dialog "Enter the UNC path:" default answer "" ¬
                buttons {ConnectButton , CancelButton} default button 1
            copy the result as list to {UNC_path, button_pressed}
     
            if button_pressed is ConnectButton and UNC_path is not "" then
                set isRunFromCommandLine to false
                process (UNC_path)
            end if
        else
            process (item 1 of argv)
        end if
    end run
     


    The run method is automatically called after the script is run, it displays a single window in which the user is prompted to enter the source path to the desired resource. The main functionality of the script lies in the "process" method. It receives the real path to the server as an input, runs the bash script described above, interprets the result and opens the resulting path in the Finder program.

    At this last stage, one problem arises. Finder has a convenient “Connect to Server” dialog box. Unfortunately, this functionality is not available through AppleScript, so after trying several different options, I settled on the “open location” method, which expects a URL to be input. And for this, it is necessary to convert the string returned by the first script into a URL representation, replacing some characters with their codes. Fortunately, Mac OS X comes with a python interpreter with libraries that have a convenient method that does exactly what we need. We feed the resulting URL to the Finder and it magically opens a window with the directory we need.

    If you try the work of these scripts, you will notice one feature: if you connect to the resource through the Finder dialog box, then the directly divided resource is mounted on the server, and in Finder you can walk through the entire directory tree in this resource. And as a result of the described script, only the directory referenced by the source path and its subdirectories are available. Like someone, but I like this behavior more: you get what you ask for, and nothing more. You need to go deeper, you can take part of the path and connect only this part, or select a connected server in the side panel of the Finder window, and from it go down the hierarchy.

    Now about the promised third way to run the script. In Cocoa programs, the Services menu is available, and it would be very convenient if our script could be called from this menu. Select the path in the text, select an item from this menu (or press the burning keys corresponding to this item) and open this path in the Finder. It turned out that this is easy to do, there is a free program (donationware) called ThisService , which allows you to connect the script to the "Services" menu. That’s why I divided the script into two methods, since ThisService requires that the script has a “process” method that takes a string as input. After a simple setup, it worked.

    There is another convenient program and just as free, called Apptivate, which allows you to assign system-wide hotkeys to launch any application, including scripts. I attached the script launch to the key combination, and now I can get my version of the “Connect to Server” window on the screen at any time.

    By the way, one important observation regarding Snow Leopard. If you already have a valid Kerberos ticket and you disable all SMB volumes, then the ticket is lost. This funny behavior leads to the fact that from time to time you suddenly find that you can’t connect to network objects, although five minutes ago everything turned out fine. This can be clearly seen if you run the “View Tickets” program (this can be done from the Keychain), connect and disconnect volumes, and see what happens to your ticket. As a result, I decided to always have it at hand, and brought a hotkey to it in the Apptivate program.

    In conclusion, I must warn that everything described was tested only on my computer (Mac OS X Snow Leopard, version 10.6.4) and only within the walls of ABBYY. Write in the comments about any inaccuracies that you notice, and suggest what can be improved. And I, and everyone who is interested, will only be grateful.

    Also popular now: