Divide and Conquer: AOT Team Beats Real Deadline for Virtual Printer

  • How-To,
  • News

virtual office printer

The adventure begins when our co-founder George calls with an emergency job for one of our previous clients. It’s not unusual for us to help out startups in this way, so we got on a call with the client’s technical team.

They’d tried and failed to come up with a virtual printer to use with macOS. And they needed it as soon as possible—in no more than two weeks, to be precise.

The basic design seemed obvious: The printer module should convert the file to a PDF and upload it to a REST API we’d design. We’d have to save a token from the API too. Not yet done, the system would have to check the API’s token and then open the Mac’s default browser to a specific URL.

So we knew we couldn’t build this virtual printer from scratch in so short a time, but we couldn’t say “no” to a prestigious client we’d worked with extensively in the past (and, of course, would love to work with in the future!). So, we readied ourselves for the mission.

Since the requirement was unusual and the deadline was tight we intended to apply a military strategy often found essential in battle: Divide and Conquer. In computer science, divide and conquer is an algorithm-design pattern that breaks down a complex task into smaller subtasks, then tackles them one at a time.

We knew that a virtual-printer driver would be a critical component of this mission. Since we just didn’t have a lot of time, we hoped to find an open-source project that we could extend. Therefore, we divided this “war” into three parts:

  1. Find an open-source virtual-printer driver
  2. Extend the driver to upload PDF to a REST API
  3. Find a solution to let the driver open the default browser.

Step #1: Find an open-source virtual-printer driver

We expected this to be the most difficult aspect of this requirement. However, after going through a few open-source projects, we found a project that met our specifications. The project we’d located was able to build a virtual printer that will convert content to a PDF, then save it to the local machine. We knew we’d be able to bundle the project and create an installer as well.

Our next task would be to get the driver to call an API. Since the driver we chose was written in C, a relatively old programming language, writing the call would be a challenge.


Step #2: Extend the driver to upload PDF to a REST API

Writing the POST API call would be our next mission to accomplish. Since the project we’d found was developed in C, we decided to use the multiprotocol file transfer library libcurl to let the driver make an API call to upload the PDF file.

static void  post_pdf(char *file_path) {
    CURL *curl;
    CURLcode curl_api_result;
    curl = curl_easy_init();
    if(curl) {
        struct string api_output;
        init_string(&api_output);
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
        curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/");
        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
        curl_easy_setopt(curl, CURLOPT_DEFAULT_PROTOCOL, "https");
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &api_output);
        struct curl_slist *headers = NULL;
        headers = curl_slist_append(headers, "Cookie: ****");
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
        curl_mime *mime;
        curl_mimepart *part;
        mime = curl_mime_init(curl);
        part = curl_mime_addpart(mime);
        curl_mime_name(part, "file");
        curl_mime_filedata(part, file_path);
        curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
        curl_api_result = curl_easy_perform(curl);
        char *status = malloc(strlen(api_output.ptr) + 1);
        char *token = malloc(strlen(api_output.ptr) + 1);
        strcpy(status,api_output.ptr);
        strcpy(token,api_output.ptr);
        status = strstr(status, "Status":"") + 9;
        strtok(status, "","");
        token = strstr(token, "Token":"") + 8;
        strtok(token, """);
        if(strcmp(status,"OK") == 0 && strcmp(token,"") != 0) {
            // Open the default browser with specific URL by appending the {token}
        } 
        curl_mime_free(mime);
    }
    curl_easy_cleanup(curl);
}

We wrote this function to send the generated PDF file to the REST API, and we called this function after the driver converted print content to pdf. And it worked perfectly!

Following that, we only needed to find a solution that would launch the browser and open it with the specified URL to open the uploaded PDF in the dashboard.


Step #3: Find a solution to let the driver open the default browser

Opening the default browser from the driver was supposed to be the easiest step, but turned out to be the longest and most difficult to finish off. Our hopes had been high as we already knew there was a Bash script to open URLs on a Mac:

open “https://example.com”

This command will basically open the default browser to the given URL. So all we needed to do was execute this bash script inside our printer driver to open the browser. Or so we thought.

Because of the limited permissions available to the printer driver, a Bash script to open the browser would not work from within the printer driver module. We checked several other Bash scripts as well by adding permissions inside the code and none of them worked.

We actually spent a couple of days trying to find a way to execute this bash script from the printer driver. And we failed. The reason was that it’s the cupsd user that was trying to execute the printer driver. Therefore, it simply wouldn’t work because the cupsd user has restricted permissions. We realized we were working on something just about impossible, so we were ready to drop this.

But yet again, just before we were ready to drop it, we thought we’d give it a final try. So we did some extensive R&D to find a solution. This time we took a different path and turned to the useful “bombs” already planted in the macOS: Folder Actions and AppleScript. Even though I’ve had a Mac for nearly a decade, I’d never used either until that moment. To win this “war” we would have to combine these two weapons into a virtual-printer package to make this mission impossible possible.

AppleScript is an English-like language used to create script files and applets that control the actions of a computer and the applications that run on it. AppleScript scripts can make decisions based on user interaction or by parsing and analyzing data, documents, or situations.

Folder Actions are essentially scripts that can be affixed to folders. They allow events to occur when items are added or removed from a folder, as well as when the folder is accessed, closed, or relocated.

Now the question was how to use these two tools in our project. Every time we print a document using the virtual printer module, the document is converted to a PDF and saved to a folder. Therefore, our strategy was first to upload the PDF to the API, save that file, and set the file name to the token we obtained from the API. Then we’d make use of a folder action written in AppleScript. When a new file is added to that folder, AppleScript will extract the file name to obtain the token, then it will create the URL that the default browser will open using a shell script run by AppleScript.

on adding folder items to theFolder after receiving theNewItems
    set filesAdded to {}
    tell application "Finder" to set folderName to name of folder theFolder
    activate
    repeat with i from 1 to count of theNewItems
        set thisItem to item i of theNewItems
        tell application "Finder"
            set FileName to name of thisItem
            set fullUrl to "open https://app.example.com/PrintDocument?token=" & FileName
            do shell script fullUrl
        end tell
    end repeat
end adding folder items to

All our PDF files will go into one particular folder, we attached the above AppleScript to the same folder during installation of the virtual-printer driver.

And it worked beautifully!

There was a catch, though. This process will work only if folder actions are enabled on the Mac. Therefore, we added one more functionality to the installer: if folder actions had been disabled on the Mac before installation, the installer will turn them on.

tell application "System Events"
if folder actions enabled is equal to missing value or not folder actions enabled then
    set folder actions enabled to true
else
    set folder actions enabled to false
    set folder actions enabled to true
end if
end tell

And to be doubly careful, we made it clear when delivering the product that it would only work if Folder Actions were enabled on the Mac.


What All We Did

Let’s take a look at everything we did to create a virtual printer to meet the requirements of the client. I’d like to divide the tasks we completed into two parts so you can readily understand them.


During Installation

  1. Obviously, when we install the virtual printer package, a virtual printer is added to the Mac and will be listed every time we print something on that Mac.
    
    


  2. Next, the installer assigns an AppleScript to the “/var/spool/pdfprinter/PDFs/uploads” folder as a Folder Action. So, When a new file is added to that folder, AppleScript will launch the default browser.

While Printing

  1. If the user selects our virtual printer during printing, it will convert the document to a PDF and send it to the API.
  2. Then the driver will save the token from the API response.
  3. The driver will name the PDF file using the token, and save the file to “/var/spool/pdfprinter/PDFs/uploads” folder.
    
    

    
    
  4. Since we already added the necessary Folder Action to that folder during installation, AppleScript will detect that a new file is being added and then launch the default browser using the base URL and token specified in the file name.

Looking Back

There were two reasons why I decided to write this article. One was to introduce AppleScript and Folder Actions to people like me who were unaware they even existed on their Mac. The majority of the time, these two tools will be useful for specific personal use cases or to fulfill unusual requirements like the one I’ve described here.

The second reason was to demonstrate how we can accomplish difficult tasks in a short amount of time. To prevent wasting time when implementing this kind of requirement, we can divide the problem into small chunks and try to implement the solution you consider to be most challenging first—because the rest of the modules are useless if that doesn’t happen.

And so much of this is about teamwork. I can’t end this article without mentioning Rajeev, our senior team member at AOT. He was a great help to me during this project.

Hope you enjoyed this glimpse at the work we do at AOT Technologies!


About the Author

Hasgar D is a senior software engineer who’s worked for AOT Technologies for the past five years. He especially enjoys the challenge of solving a difficult task and specializes in system design and integration. Outside of work hours he enjoys playing badminton.