Thursday, May 08, 2014

Desperate Measures - A Time To Hack

A Case Study


Sometimes the features that seem simplest on the surface are the hardest to implement. This statement rang true for me in the past few weeks. In today's post I will walk you through the stages of my latest feature request and the challenges I encountered. This example is far from my typical experience, but is an extreme example that illustrates the kind of problem solving and outside-the-box thinking required of professional software developers.

For a bit of background, my work involves payment card processing in various forms and across a wide range of systems and third-party applications. One of my most successful projects to date has been a full payment solution integrated with a third-party point of sale system. This new feature was an enhancement to that system and required customization of receipt printing as requested by a client.

In a typical setup, the system will print three receipts for each credit card payment: a merchant copy, a customer copy, and an itemized receipt listing all of the items and totals ordered by the customer. Our client must be environmentally friendly (or, perhaps, simply cost-conscious) because they asked to eliminate as much receipt waste as possible. When configured for this new feature, the system would print nothing by default. Instead, it would cache the receipt details for later printing if requested. "Hey, could I get a copy of my receipt?"

At first glance, this feature sounded pretty simple. Creating a cache to store data for later retrieval is the bread and butter of a developer's work with many tried and true solutions. I would have to research the point of sale system scripting language and communications protocols to install a new button that would fetch the receipt details from our cache and print the receipt on demand. This, I figured, would be the challenging part of the project. While challenging in its own right, this paled in comparison to one challenge that I had overlooked.

The overlooked piece was the itemized receipt. These details are normally printed automatically by the third-party system. "No matter", I thought, "there is an API call to access these details". I had used it in a couple of different places previously. It wasn't until I went to implement the API call that I discovered the problem. The API did not work when a payment was in progress or after the check had been closed on the system. This meant that by the time my software got involved with the payment, it was already too late to retrieve the itemized receipt details.

Desperate times call for desperate measures. We contacted the vendor to discuss possible workarounds. After some back and forth discussion, we learned that the system has a print-to-file receipt option. I figured this would be a good option. My software could monitor the receipt file, waiting for new data to be written. I could then parse and cache the latest receipt details for retrieval. I spent some time restructuring the code to monitor a file for updates and scrape out the itemized receipt details.

After struggling to learn how to monitor a file for changes, all went well with my initial test runs. The receipt text was written to the file. My software read the new data and cached it. Hitting the receipt button triggered the system to print out all of the required receipt details. I figured it was just a matter of tidying the code and running it through a battery of tests. I sat back and admired my clever solution to the problem, proud of implementing a reasonably complex solution to my seemingly simple problem. It wasn't until we brought a second machine into the mix that signs of a flaw in my plan appeared.

The point of sale system runs with a central server and multiple client workstations. I had performed the majority of my development work on the server for simplicity. On the server, everything was working seamlessly. On the workstations, we saw random delays from the time of processing to the time the file was actually modified. Sometimes it would take a few seconds. On other tests, the delay climbed nearly to the 1-minute mark. Waiting a whole minute for a receipt simply wouldn't fly with a busy merchant.

Further discussion with our vendor contact led us to conclude that this was simply the nature of the system. The file-based printing was designed as a record-keeping system, not a live processing environment. It was time to go back to the drawing board. Something about the best laid plans...

While discussing the problem with a colleague, I joked about it saying, "maybe we should just write a printer driver." My colleague took my joke a bit more seriously than I had expected and started playing around with the printing configurations on our test lab. He found a printer option identified as TCP/IP printer and pointed it at our software's listen port. We ran a test transaction and saw that some unknown request data had been logged by our software.

Somewhat bewildered and whimsically bemused at what I was about to try, I visited everyone's favorite search engine and started looking around for TCP/IP printer message format specifications. With a bit of trial and error and some luck, I stumbled upon a specification that looked similar to the messages that my application was receiving. "This might just work...", I thought.

According to the specification, the first message I saw was a printer status message. I updated my software to respond with a "ready" response. When I received a second request following my response, I knew we were onto something. I worked through the commands one by one until I saw the first line of the receipt header in our logs - plain as day.

There's something about this particular hack that is both wonderful and horrifying at the same time. I get a thrill out of the fact that we are simulating a printer simply to retrieve the information we need. At the same time, I boggle at the fact that something this simple shouldn't have to be so hard. There is also, unfortunately, a nagging risk that my software will be incompatible with some requests sent to it in the field. We will do our best to reduce this risk, but doing something that's not officially sanctioned and ordained by a vendor is generally a bad idea. Whatever way you slice it, this solution works!

Perhaps my favorite part of this solution is how I imagine the message exchange to determine the printer status:

THIRD PARTY:  Do you have paper?
MY SOFTWARE:  I have a roll long enough to span the country.

THIRD PARTY:  Is the paper door closed?
MY SOFTWARE:  It's glued shut.

THIRD PARTY:  Do you have ink?
MY SOFTWARE:  A well of ink so deep you could swim in it.

THIRD PARTY:  Is your cutter enabled?
MY SOFTWARE:  The blade is sharp enough to cut diamonds.

Hacks


Why do I have two simultaneous and opposing reactions to this hack? Why would some developers turn their nose up at my solution, while others would describe it by saying, "that's brilliant"?

The software industry has a long tradition of describing both clever and ugly solutions as "hacks". This is, I believe, because most programmers are very calculating people. We tend to have strong feelings about the "correct" solution to a problem. When something deviates drastically from that ideal, we describe it as a hack. At the same time, when there is no correct solution to a problem, the curious among us try to come up with a "clever" solution to work around the barriers set in front of us. We will often describe this as a hack as it deviates from the theoretical ideal solution in the same way.

Whether or not you like what I've done in the case study above, it's hard to argue with results. If an ugly hack is what stands between myself and my goals, I'll choose the hack every time (barring the ethically questionable). When there is no correct way to do things and I can find a workaround, you can bet that I will take the workaround. On the other hand, if there is a correct way to do things that cannot be accomplished under some immediate time pressure, you can be sure that I will want to come back and set things right when I get the chance. Remember, the customer is always right. The client doesn't care about your academic ideals -- they want results.

Quality developers usually need to think inside the box. The best developers will look at standard approaches to their problems. They will use proven technologies and techniques to get things done in a way that is robust and maintainable for the future. The best developers, however, have the ability to think outside the box when necessary. The resourcefulness and ability to bend a software system to your will is what separates the great from the merely good.

What is craziest hack you have seen or implemented? Tell me your story in the comments.

Cheers,

Joshua Ganes

6 comments:

  1. I can tell you that some company in London (UK) is using the same sort of trick to get receipt data out of the POS, you are not alone!

    ReplyDelete
  2. I think my favourite example of a "dirty hack that gets the job done" was by the developers of the DOS game Wing Commander. The game worked fine, but when exited, it output a strange error message to the console, complaining about some virtual memory issue. They couldn't figure out the cause, and it didn't seem to be hurting anything - so they opened the executable in a hex editor and changed that message to "Thank you for playing Wing Commander".

    ReplyDelete
  3. My software maintainer self cringes at this. My hacker self revels in the fact that this is the most glorious hack I've ever seen.

    ReplyDelete
  4. I love the printer status dialog. However, I must disapprove of gluing the paper door shut. The roll of paper that spans the country will run out after you print 43 million or so sheets, and then you have little choice but to replace the printer. Therefore, to conserve the lifetime of your non-physical printer, I recommend printing on both sides of the imaginary paper :)

    ReplyDelete
    Replies
    1. I'm simulating a thermal printer. They only work correctly when the paper is loaded right side up. I'd better make sure I simulate the correct side of the receipt paper for both big endian and little endian systems :)

      Delete
    2. I'd be curious to see the error message on that.
      POS Error: Please load paper into the feed slot.
      Cashier: Ok, um, but this thing doesn't have a slot!

      Delete