in Projects

Automatic Rainbows and fuse-colors

This post covers some of the neat speed bumps I hit while developing fuse-colors.

Dynamically Generated Perl Scripts

When someone requests a file from fuse-colors, it returns one of three types of output. If the binary uses ncurses, it returns a perl script that runs the command unmodified. Otherwise, it returns a perl script that runs the “format string”, replacing the token “__command__” with the command they requested. Lastly, a special case: accessing ‘refresh-ncurses-cache’ forces the mount to update its list of bad binaries.

One of the major benefits of having the user set their PATH variable to the mountpoint is that builtin shell commands still run fine. Before the mount even gets a chance to run, the shell will check-for and run any important builtins or aliases. It lets you keep the full power of the shell. It’s also nice because you can still fully-qualify paths if you really want to skip fuse-colors.

Another interesting point is how perfectly this is able to handle the command and it’s arguments. The FUSE API lets me implement a read call, handing me the filename that was requested. I generate a perl script that runs the filename. The generated perl script also has arguments passed to it. So if someone tries to run “ls -lha”, a perl script gets generated that calls “ls”, and the parameters that were passed into that script are “-lha”. So I can join in @ARGV in the child script and fully replicate the original request.

I had an infinite loop in an early implementation. Whenever I ran a command, it would pause until I ran out of processes on my system. The generated perl script would run “ls”, which would look under the fuse-mount, causing another hunt for “ls”, and so on. Keeping the $PATH saved from the launch is important. The perl script that gets generated specifies a $PATH for each command, using the one that was available when fuse-mount was launched.

The commands run in a subshell. Since the user can specify their own format string (instead of “__command__ | lolcat”), we need to be sensitive of format strings that invoke multiple semicolon-delimited commands. The subshell lets us keep the temporary $PATH for the whole duration of the command.

The FUSE framework allows you to implement any normal filesystem calls, including reporting how large a file is. I originally put a static 100 bytes. At one point the mount stopped working because my generated scripts went over 100 bytes and weren’t getting fully read. I lazily bumped it to 400 bytes and opened an issue promising to dynamically calculate the size.

If you call a command that doesn’t exist, the perl script gets generated, and you’ll still get the same old “Command doesn’t exist” response. The generated perl script will “exec” the command, it will fail, and report the result like normal. By default, fuse-colors doesn’t mess with STDERR.

The only interesting FUSE-specific note I have is the “allow_other” setting. This flag lets you share a mount between users. It defaults to off, so if user “A” mounts fuse-colors, user “B” can’t touch it. I had to specifically enable “allow_other” for other users to be able to access the mount. It’s more fun when you’re root and you want to play with people.

Parsing ncurses binaries

This was pretty straightforward. I need to make a list of binaries that shouldn’t be altered. Binaries that use ncurses use the terminal for a GUI and are typically bad candidates for pipes. The steps were straightforward:

  1. Figure out every binary under $PATH
  2. Run “ldd” on the list of binaries. Saves time to do it once instead of spinning up hundreds of processes.
  3. Parse the output and look for ncurses.

I had originally used objdump instead of ldd, which was mostly because it was the first command I remembered that worked. ldd is the more-proper command to use even though it’s nearly twice as slow. OSX doesn’t have ldd though, so I also had to write a conditional for OSX. “otool” does the same thing on OSX, and its output is close enough that my regex stayed the same.

The only self-gripe I have is my insistence on using File::Find instead of just globbing. I used GNU find to prototype the parsing and habitually transformed it into File::Find when I wrote the perl. Globbing would have been fine and easy.

Ruby default binary location

The original intention of fuse-colors was to make every command’s output colorful. lolcat is a Ruby gem, and gems don’t get installed in the same location everywhere. It turns out the right way to determine the path is to run this:

ruby -rubygems -e ‘puts Gem.default_bindir’

Thanks to SuperUser for this…gem.

Custom format string

The format string code itself is dumb and just substitutes the literal “__command__” with the filename that was requested, plus the joined string of args. The custom string opens up some interesting/fun commands, like the makeshift command logger from the README:

./fuse-colors.pl ~/fuse 'echo "Command is: __command__" >> /tmp/commands.txt; __command__ | tee -a /tmp/commands.txt'

Combining this and something like a restricted bash shell (rbash) could be useful for something, I’m just not sure what yet. There are definitely better solutions for this specific problem, but I think having this capability might answer other problems. fuse-color’s capability is very similar to ZSH’s preexec function, but the ncurses protection is nicer and this doesn’t require a specific shell.

If fuse-mount had a customizable filter option, you could create several purposeful fuse-mounts that intelligently decided when they should come into play. This could come into play in several different ways, like:

  • context-aware terminal sessions. if you run “ls” three times in 30 seconds, open a new screen session with a “watch ls” running
  • pattern detection. “they’re running strace and curl and tcpdump, start auto-teeing output to a file”
  • user spells command wrong in the same way all the time and ZSH didnt autocorrect and error code was set, suggest auto fix
  • perl one-liner keeps getting run but isn’t aliased, auto commit to one-liners repo
  • vim <file> and ./file keeps happening, automatically update to “dev-hours-spent.txt”

Some of these are scary, but they’re options that weren’t easily accessible to me before. Seems like it could be fun!

Write a Comment

Comment