One thing I forgot: in the book Ansible Up & Running, in chapter 3, there’s a Vagrantfile describing three containers with consecutive ports exposed and mapped to the host OS.  Each VM is defined individually, by hand.  I had read that a Vagrantfile is just Ruby, so I thought, “Screw that.  I’m gonna use a loop.”

It turns out you can’t just use a loop.  Because the VMs are booted asynchronously, they need closures.  You can replace the entirety of the Vagrantfile in chapter three with this:

config.ssh.insert_key = false

(1..3).each do |host|
  config.vm.define "vagrant#{host}" do |vagrant|
    vagrant.vm.box = "maier/alpine-3.4-x86_64"
    vagrant.vm.network "forwarded_port", guest: 79 + host, host: 8079 + host
    vagrant.vm.network "forwarded_port", guest: 442 + host, host: 8442 + host
    vagrant.vm.synced_folder ".", "/vagrant", disabled: true
  end
end

The do |host| syntax creates a closure that ensures that the host variable will be different for each VM, run in a separate scope. Using a for loop, the host variable would be “3” at the end and when vagrant picks up the entry, all three VMs will try to allocate the same port, resulting in conflict and crash.  Again note that I’m using Alpine here.  If you’re using Ubuntu as the book recommends, make sure you change the container name.

For professional reasons, I’m working my way through the O’Reilly book Ansible: Up & Running. The book is pretty good, explaining the basics of Ansible so far as well as running me through a couple of exercises. The book uses Vagrant as its target container, which is fine by me. As is my wont, I made my life harder by changing things up: rather than using Ubuntu as my target OS, I chose to use Alpine, for which the base Vagrant image has no instance of Python. I also chose to automate my generation of Ansible’s hosts file.

The only secret to using Alpine over Ubuntu is simple: Alpine doesn’t ship with Python, and Ansible requires Python. Ansible has a work-around, though: RAW mode.

#!/usr/bin/env ansible-playbook

- name: Install Python on Alpine
  hosts: servers
  become: True
  gather_facts: False
  tasks:
    - name: update apk
      raw: apk update
    - name: install python
      raw: apk add python

Commands delivered raw will be fed directly to the shell, and so don’t require Python. They’re treated almost, but not quite, like -m command style commands. The latter still run through Python, which still seems odd to me, but Ansible is an okay tool and I’m not about to try and re-write it to my satisfaction.  become is still necessary, as installation commands still need to be run through sudo, and gather_facts must be False for this pass as fact-gathering is performed with Python.

Automating the translation of Vagrant’s auto-generated SSH configuration to something resembling Ansible’s host file output was trickier. My first thought was to use Perl, but then I remembered that when it comes to multi-line patterns, Awk is still the master of the game. I use Gawk here, but that shouldn’t make a difference.

vagrant ssh-config | \
    gawk 'BEGIN { RS="\n\n"; FS=" " } \
          /Host / { print $2 " ansible_host=" $4 " ansible_port=" $8 }'

Alpine’s layout of nginx, which is the server used in the tutorials, is different from that of Ubuntu. I’ll leave it up to the reader to figure out those differences. That shouldn’t be too hard.

I was reminded this week of Steve Yegge’s Programming Dirtiest Little Secret, in which Yegge claims that touch typing is the most significant skill a programmer can have, because the very mechanics of hunt-and-peck typing use up brain power that the professional programmer cannot afford to waste. Yegge says that the important stuff of programming, you have to have completely mastery of the device of expression; your computer must become an extension of thoughts to be expressed, not a tool to be managed.

I’m a touch typist. I wrote 2400 words this morning in about 90 minutes, without looking down once at the keyboard. I can’t look down at the keyboard; it won’t help, as the keys still read QWERTY and the mapping is set to Dvorak.

I am also, by any measure, an exceptionally good Python programmer. When I need to do any kind of data processing, web programming, scientific visualization, I reach for Python. It has all the libraries I could possibly want, it has a syntax that is reachable and easy to grasp, and it’s just plain hard to get wrong. I know the inner workings of the Cython VM to a ridiculous degree, have a passing knowledge of how Stackless and PyPy work inside, and can go on at absurd length about the brilliance and historical brokenness of Python’s import function. My Stack Overflow reputation is over 12 thousand, and half of that comes from answering Python questions.

The other day I wrote a small utility to analyze my personal projects directory and spit out a one-line description of each project, or “No description found” if it couldn’t figure out the description. It took about an hour. During that hour I:

  • looked up the documentation for os.path
  • looked up the syntax for Python’s with/open
  • searched for how to find the HOME directory in Python
  • searched for how to set maxdepth on os.walk

That is, during that hour, I knew that each of these features existed, but I didn’t know how to use them. I have places in my head where the vague knowledge that these capabilities exist resides, but no real library that lets me express myself fully without having to pause and go read some documentation or blog entry an how to do such-and-such.

If you read any advice on how to win customers, the advice is always: details, details, details. In this industry, though, there’s almost never time or money for the details. Things move too fast. We develop the minimum viable expertise necessary to create the prototype, at which point the sales department starts shouting, “Ship it!” We never have enough time to learn how to express ourselves in a given development environment before everything changes again and we start the learning process all over again from the beginning.

It is this ability to express our intent that we need to fully develop. We need to slow down and master our development environments before we can earn the time needed to attend to the details. Otherwise, we’re simply at the mercy of market forces to go faster and faster, and we’re all going to lose that race in the end.

10Jul

Shiny New Linux Surface Pro 3!

Posted by Elf Sternberg as Uncategorized

So, after a heartbreaking month of having a Surface Pro 3 that was utterly unstable and unusable, I now seem (emphasis on seem) to have it completely stable and successfully running an instance of Linux Mint 18.2.

Required Equipment

Here’s all the things I did, probably in the order I did them. First: buy some stuff: A powered hub with at least four ports, a USB-to-Ethernet connection, and two 16GB USB memory sticks. If you want to preserve the installed Windows instance, you’ll need a second 32GB USB memory stick. (You’ll have to go on-line to find instructions on how to back up your Windows instance.)

I assume you have a USB-capable keyboard and mouse. Download the latest version of Linux Mint. For Ubuntu-based distributions, you must also go and get more up-to-date kernels. As of this writing, I used the Linux Kernel 4.12.8, amd64 versions. There are five files in that collection; the three you want are the headers, the generic headers, and the image; skip the ones label "lowlatency".

With another Linux computer, use unetbootin to install the Linux Mint ISO on one of the 16GB sticks. Do not reformat the drive to a Linux format; let unetbootin install the contents of the ISO into the FAT32-formatted stick.

The other 16GB stick can be any format Linux recognizes, and Linux recognizes FAT32. Copy the linux kernel files you downloaded onto it. You will also want to copy the latest versions of the MRVL firmware onto the stick to update the Surface Pro’s wifi firmware.

Installation of Linux Mint 18.2

Plug the hub into your Surface Pro. Into the HUB plug: the ethernet cable, hooked to a live network; a keyboard; a mouse; the 16GB stick with the bootable ISO on it.

Turn on the Surface Pro while holding down the "volume up" button; this will give you access to the BIOS. Disable "Secure Boot" and "Trusted Platform," and changed the boot order to USB → SSD. Exit and let the boot process proceed. If you’ve done it right, you’ll get the "Welcome to Linux Mint!" page. Do your thing: Install Linux Mint. I just blew away my Windows install; I didn’t care to keep it. That’s not the purpose of this exercise anyway.

When I tried to reboot, nothing worked. It kept insisting that there was no bootable medium! I found this solution, but I had to change hd2 to hd1 as I am not dual-booting. Once I was in to my instance of Linux, I was able to see via efibootmgr that the master boot record was corrupted; I installed and ran YannUbuntu’s boot-repair, and to my pleasure it actually fixed the problem the first time.

I rebooted back into the BIOS and switched back to "Secure Mode" for boot, and continued, and it worked fine. Yay!

Upgrade to the latest kernel

My hopes were dashed, though, when the network proved to be just as flaky as it had been earlier. But I had bigger problems: the keyboard and touchpad didn’t work. To fix that, put in the memory stick with the linux kernel image on it and type:

dpkg -i linux-*.deb

(If you downloaded more than one kernel, you’ll have to find a way to distinguish them.) Now reboot. If you have a kernel past 4.11, your keyboard, touchpad, and touchscreen should now all work. Yay!

Hibernate and Wifi

I did get the wireless working, too.

  1. I updated the firmware to the latest versions by copying the files in that directory to /lib/firmware/mrvl
  2. I added to /etc/NetworkManager/NetworkManager.conf:

    [device] wifi.scan-rand-mac-address=no

  3. I edited /etc/NetworkManager/conf.d/default-wifi-powersave-on.conf and changed "wifi.powersave=3" to "wifi.powersave=2".

The one thing I did not do, which I did last time, was blindly drop my old .config directory onto the new machine. The last time I tried this, I built the box, then restored from backup my personal account from my old Lenovo Yoga. This included all the keys, passwords, configuration details and some cached data (because idiots keep using .config as a cache directory; I’m looking at you, Google Chrome!) from the old NetworkManager, and it appears that some of those changes were what broke the old one.

The next step is to disable "sleep mode"; the Surface Pro doesn’t have a sleep mode! Instead, it’s either "off," "on," or "connected," the last of which is much like your phone when the screen is off: still processing network events and handling notifications. The Linux kernel isn’t up-to-date on this state, and almost no applications understand connected mode, so it’s useless. The best we can do is hibernate. This isn’t that bad because the Surface Pro has an SSD; booting to a hibernated screen is about 15 seconds, not terribly shabby. Not the ~3 seconds of my Yoga, but livable.

On Linux Mint 18.2, these commands were:

cd /lib/systemd/system
sudo mv suspend.target suspend.target.orig
sudo ln -s hibernate.target suspend.target

This links the systemd (gag) suspend target to hibernate, so that commands to suspend trigger hibernation instead. It’s actually pretty functional. Unfortunately, the mwifiex driver is still flaky, so you’ll have to handle it by hand. The following script goes into /lib/systemd/system-sleep/mwifiex, and has to be chown root mwifiex; chmod 755 mwifiex in order to run correctly.

#!/bin/sh
set -e

if [ "$2" = "hibernate" ]; then
    case "$1" in
        pre) modprobe -r mwifiex_pcie mwifiex ;;
        post) modprobe mwifiex_pcie ;;
    esac

Now, theoretically, you have everything. Hibernate works and recovers; wifi comes back.

Bluetooth

Bluetooth works out of the box with the 4.13.0 kernel, but the LE features only work with the very latest version of bluez, and even then you have to modify /lib/systemd/system/bluetooth.service, changing the Exec line to this:

ExecStart=/usr/libexec/bluetooth/bluetoothd --experimental

Adding the "experimental" line will let things like Bose products work with bluetooth.

Also, if you’re a pulseaudio user (and if you’re on Ubuntu or Mint, you are), add this line to the end of /etc/pulse/default.pa, and when your headphones connect Pulseaudio will automatically re-route the audio, rather than your having to do it manually.

load-module module-switch-on-connect

Cameras

The cameras worked out of the box. I played with Cheese and it liked both cameras.

Autorotate

The best autorotate script I’ve found yet is Ayko Poel’s SurfacePro3 Scripts. It does automapping of the pen for you, which is really nice.

Touchegg

I really like Touchegg. I use QComicbook to read hentai comics, and by configuring both autorotate and touchegg’s configuration, I get an easy, pagable interface that I can use to flip through the pages as fast as I can.

Gimp and Krita

Both worked out of the box. Pen pressure worked out of the box, but getting the eraser and the button required a little configuration. Now that they’re working, though, I have a pretty good casual art pad as well as a decent writing machine and even a competent coding machine.

Next steps

I’d like to get autobrightness working. The bluetooth configuration is still a little flaky, but better than it used to be. One thing I’d really like to get is to map the accelerometer and/or the gyroscope devices to a joystick device, so that you could use the tilt of the surface pro as a kind of joystick. I once wrote a lightsaber toy that used the old Thinkpad HDAPS sensor as a joystick, and porting that to the Surface Pro (and maybe to Rust) would be a pretty good exercise.

19Jun

“What Motivates You?”

Posted by Elf Sternberg as Uncategorized

I won’t link to the Orange Website for Hacker Bros, but a question asked yesterday has collided with a conversation I had had ealier this week. The question was: “What motivates you to do what you do?” I had a simple answer. These guys:

disgust-anger-fear

If you’re not familiar with the briliant Pixar film Inside Out, the film depicts an adolescent girl named Riley, and her internal struggle and emotions as she comes to grips with her parents’ decision to move to the big city. Those are three of  Riley’s five primary emotions, from left to right: Disgust, Anger, and Fear. (Not shown: Sadness and Joy.)

Almost everything I’ve ever done has been due to an emotional reaction, usually one of the three above. While I find my fiction writing to be a joy and a pleasure these days, three million words started with disgust: “Good grief, people, why are you liking that horrible Brady Bunch fanfic so much? It’s terrible! Anyone can do better than that. Let me show you.”

A lot of my smaller contributions to other open source projects have been due to anger. I was angry when my joystick didn’t work after getting Freespace compiled on Linux, and thus an obscure driver was born; I was angry when I couldn’t get my porn off Usenet, and thus the bugfixes to Python; I was angry when Evernote changed its policies, and thus my Enex ripper; I was angry when Delicious shut down, and thus my Delicious ripper. And so on.

The biggest emotional reaction I have, though, is fear. Fear that I’ll be obsolete someday, fear that as I get older, I won’t be so good they can’t ignore me, no matter how many grey hairs I’ve got, fear that my skillset will become irretrievably outdated if I stay in one place for too long.

I actively envy a lot of developers who find cool stuff to work on and really seem to get a thrill out of the long-term chase. I wish I did. I do get small frissons here and there, and there have been times where I have glimpsed something rare and wonderful, chased it down and admired it for what it was. I love learning new things.

Mostly I love how learning new things keeps the fear at bay. Which is probably not healthy. But it is what I’ve I got.

I’ve been struggling with the notion of theories and provers in the context of computer programming for a long time now. I was reading Peter Naur’s general concept of Programming As Theory Building and I don’t see how it connects to Type Systems at all.

In Naur’s thesis, there are three kinds of objects: (1) The world of physical things and events, (2) the world of mental objects and events (qualia?), and (3) the actual products of thought such as theories, stories, myths, tools, and social institutions. Programming, Naur says, is about the creation of objects of the third kind: the reification of thought products into processes. It isn’t enough to be intelligent; one can intelligently and skillfully perform all sorts of actions such as driving, typing, dancing and so forth; one must also have a theory, a context backing your actions such that you can meaningfully and coherently explain your theory, answer questions about it, argue on behalf of it.

In Naur’s Theory Building View of Programming (1985), the act of programming is the construction of a theory about how human processes can best be supported by one or more computer programs. This sorta coincides with Yourdon’s premise (1989) that one can’t begin to cybernize (Yourdon’s word) a process until one clearly understands the process he is seeking to enhance or replace. Prior to the Age of the Internet, Systems Analysis and Design was a serious discipline (I’m a Trained Cyberneticist™, kids, don’t try this without professional help!) in which the first step was to completely document the end-to-end product of some paper-pushing organization, and then try to figure out how to best enhance or replace that organization’s paper-pushing with a more efficient computerized version. It wasn’t enough to replace notebooks and carbons with keyboards and screens; sometimes, you had to replace whole systems with streamlined or adapted versions. We were taught that Analysis and Design was a holistic discipline, not just a programmatic one.

One place where I think Naur breaks down, and this may be due to the tools and processes available to him at the time, is where he says that he believes it is impossible to embed enough of the Theory (the outcome of the original team’s analysis and design) in any given program for it to be clear to other programmers without further input from the original team. The Theory is clear only in the team’s collective intelligence as they develop the program, and the program only articulates some mechanistic processes that support the theory.

I also believe that Naur falls down in the face of modern development because the disciplines of programming simply weren’t all that great in the 80s. Too much of it was dedicated to trickery to get more performance out of the system; the Theory embodied not just what the program had to do, but specialized assumptions about the hardware that would support it. Finally, programs today are built from tens, perhaps dozens, or even hundreds of libraries; I don’t believe we actually need a Theory for each library to competently use them in our productions.

I do believe we need a Theory for our own production, to explain why we need so goddamned many libraries, however.

Naur claims that few software development houses work according to a Theory. He may be right. There are tens of thousands of "IT shops" in this country, and most of them don’t really care. They just want to develop Yet Another App, their knowledge of the underlying technologies, be they HTTP, SSL, virtual machines, containerization services, and databases, is so tenuous as to amount to empirical daemonology: poke it and see if it pokes back. But that doesn’t matter, as CPU power is cheap and there’s enough scaffolding holding the whole leaking, clanking, steaming, whistling contraption to lurch forward like Howl’s Moving Castle, such that they don’t have to care. There are days when I feel like I’m working with a theory that is, at best, vague and hard to define.

As near as I can tell, the terminology of "theory" in Type Systems has some overlap to Naur’s: The programmer uses Types to constrain what the program does to fit within the Theory in the programmer’s head, and uses the Type Checker to assert that the Theory is complete and comprehensive, but I don’t think that the notion of "provers" in Type Systems has anything to do with Naur’s thesis.

Naur’s Programming as Theory Building is still relevant 30 years after its publication (and the year I started my CS degree program), but it describes challenges most of us no longer face, and was written at a time when the cognitive support tools were a desert wasteland compared to what we have now. It’s worth reading, and it’s worth supporting, because if you write software and you don’t have a clear theory about what the program will be and what it will do, you can’t write a good program.

So, I discovered a little miracle the other day. I’ve gone in heavily for Emacs org-mode, which is about 50% of what I want. It’s not terribly visible, and what I really want is something that is heavily web-enabled, but in the meantime it’s "good enough." Ever better, there’s a good Org Mode to-do manager for Android applications, Orgzly.

But what really makes it worthwhile is Syncthing, a "Personal Dropbox" that syncs up directories and folders just the way dropbox does. Everything is encrypted, and the bandwidth use is actually fairly low once the system is synced up. By running Syncthing on my laptop, my desktop, and my phone (and there is an Android app), I’ve finally got a set-up that gives me the power to manage my to-do lists with a reasonable front-end, a powerful syntax that I understand, and access from text editors, command lines, graphical UIs, and web-based interfaces.

The reason it’s not perfect, in my opinion, is that Org Mode is a sprawling utility that doesn’t do a good job of associating bookmarks to to-dos. And let’s face it, most of the time when we bookmark something, it’s associated with a personal project or task that we want to accomplish, even if it’s "Read this later for pleasure." It would be nice if Org-mode had tagging, and we could use the tagging ourselves. That doesn’t exist yet, and it would be fun if it did. Using at least limited machine learning to say, "This bookmark is associated with these projects" would be super-awesome.

Someday.

For now, though, I’m going to have to change my Wiki thing into an org-mode system, and then port my Delicious and Evernote archives to that system. Won’t that be fun?

10Apr

Posted by Elf Sternberg as Uncategorized

I realized this morning that, out of a team of eight people, there are only two dudes: me, and the guy who does the documentation. The project manager is a woman (who also writes code), the three other coders are women, the two QA personnel are women, and our UX designer and quality control is a woman.

The only thing that separates any of us from another is experience. I’m the oldest programmer, with a crazy amount of experience, and I tend to surge straight ahead for the most obvious solution, the one I have stored away in my archive of experience. “Always with the elegant code,” one said. “You use chain and lift way more than anyone else I’ve ever met. I’m not complaining, but sometimes I think you make us look bad.”

I hope not, because every one of them is a great developer. And I admit, I am sometimes lazy. I have a checklist that I sometimes fail to go through to make sure that even if I have met the acceptance criteria, I haven’t gone above and beyond and looked for non-obvious problems with the code. I’m not a “10x programmer;” I don’t believe they exist, and the ones who are sometimes claimed to be such leave behind a mess it takes 9 other “1x” programmers to clean up. I don’t want to be that guy. That’s part of the reason I wrote git-lint, to make sure I literally could not check in anything that doesn’t meet a certain minimum quality standard.

There isn’t a developer on this team who isn’t completely competent to do the job. Every single one of them produces fantastic software that meets or exceeds our quality expectations and does the task. Our work isn’t the most glamorous— we’re currently building an automated inventory management system. But it has to work, because people pay us lots of money for it.

Every workplace has had people who can’t do the work. In my thirty years as a software developer, I’ve seen it all: the one with the bad drug habit that got worse, the not-so-subtle drunks, the just plain lazy flakes. The sad fact is even those people were highly skilled, but they had emotional problems that held them back. Not a single one of those people was distinguished by their sex, though. Ninety-nine percent of the people I’ve worked with have been able to do the job competently and diligently. Almost everyone who has a knack for programming can do the work if they feel safe and justly compensated. It really doesn’t matter what color, sex, gender, or religion they are.

One day, some of my teammates will have had thirty years of great experiences, and be able to teach the next generation about elegance and diligence, the technical and the people skills. I know I could still use work on my “people skills!” (The ones with great people skills will probably be moved into management; I can’t say that’s good or bad, but I’ll be sorry to see them go.) Someday they’ll be able to just roll out the answer, because thirty years of experience will give them libraries of “known good” solutions and the habits of insight needed to apply them. Sexism, especially in software development, just confuses me. Brains are brains. The women are as good as the men. I would even describe several of either sex as damnably brilliant, and worth learning from. I’m just… what is it about some dudes that they can’t accept that?

Last year, I worked my way through Lisp In Small Pieces, implementing several variants of the Lisp engine Queinnec described, most of them in Coffeescript. I’ve decided this year that I’m going to continue building out my language experience and write a scripting language. I’m still working out the details, but I’m working my way now through Scott’s Programming Language Pragmatics while reading a lot of extra stuff on the side.

One of the gems I found recently was a copy of the Smalltalk-80 Implementation Guide. It is, to say the least, a fascinating book. It introduces a virtual machine in the context of programming in 1980, which was 37 years ago, and the assumptions the authors make as to what I’m expected to know as a potential developer of a Smalltalk environment fascinated me.

The book describes a stack-based, tree-walking virtual machine. As Smalltalk is a purely object-oriented language, the tree is completely reified by the OO environment; you choose an object and a function to start, and the interpreter walks that function’s statements and subroutines until execution ends at the end of the start function. Methods are dispatched according to a lookup table, and primitives are encapsulated in a large switch() statement.

What fascinates me about reading this is that I know, at least theoretically, so much more about writing VMs. The VM described here is a nightmare of cache misses and branch prediction failures; fully a third of a modern CPU’s efforts will be going into loading, evaluating, and then disposing of anticipated operations that the VM will throw away unused. There’s no mention at all of JITting the program’s functions (converting long sequences directly into machine code) or even just its spine (writing the sequence of primitives as indirect, or even direct, unconditional branches such that the CPU’s branch predictor works most of the time; direct is cool but requires the primitives themselves be copied and modified for direct uncoditional branching back to the spine).

Since in Smalltalk almost any object could be a launching point and since relationships between objects can be changed with the swipe of a mouse, the speed with which we’d have to recalculate any given set of relationships would have to be a priority. It would have to have Go’s internal speed of recompiling, only to put the results into an executable anonymous mmap()’d space, and then map everything than needs it to be able to find that code.

A modern Smalltalk VM could be a marvel to use. It would be a nightmare to write.

Be that as it may, choosing to write programming languages today, even ones on top of a system language like C or Rust, is to confront an embarrassment of riches. There are so many interesting ways to do something now. We have JITs! We have Algorithm W! We have concurrency problems like you wouldn’t believe! 37 years ago, they had two options: compiled code, or a stack-based switch statement on top of compiled code. The writers assumed you knew more about those two ways, and absolutely nothing about all the other stuff.

Joe Wright has an excellent blog post about Anti-If Patterns that I naturally believe everyone who write programs ought to read, but there’s a detail that’s missing that I think is absolutely crucial to getting anti-if programming correct.

Types.

Yeah, you can yawn and leave now, if you don’t care. But I’m with Guido von Rossum on this, that once your software reaches a certain size the only way to stave off chaos is to demand more of the programmer up front, and that demand comes in the form of requiring programmers to understand the shape of the data, and to make sure that the pieces being passed between units of code fit perfectly.

Wright glances off the issue in his second pattern, “Use polymorphism instead of switch().” This is a common piece of advice, although it really only works when you have more than one switch statement; at that point, your switch statements are collections of methods that apply to different objects.

It’s his first (Boolean Params) and fourth (Conditional Expressions) patterns where he falls down a little. The most critical issue in both of these is the shape of the data. “Boolean Params” is just “Conditional Expressions” written as a lookup table. If we play the classic programmer exercise of zero, one, or many, a lookup table is a conditional expression taken from the “one” state to the “many” state. It is therefore absolutely critical to put your foot down and state for the record, In any conditional expression, for all sub-expressions, all left-hand values must share the exact same type, and all right-hand values must share the same type.

If this isn’t the case, you’ve created a way to sneak if back into the system, with separate code paths that must be unit tested. And down that road lies madness and unreliability.

Subscribe to Feed

Categories

Calendar

October 2017
M T W T F S S
« Sep    
 1
2345678
9101112131415
16171819202122
23242526272829
3031