Category: Uncategorized

  • Bladder training app progress

    I’ve gotten to a decent beta at this point, and I’m eating my own dogfood: my urologist put me on bladder training with a two-hour interval, so the app is set up with that as the default interval.

    I’ve been testing it as I go along and added a few things that I needed:

    1. Custom alert sound. I get enough alerts that I need it to sound different than the others. I’ve got a nice water-drip noise, distinctive but not obtrusive, that fires when the interval is up.
    2. The process is not loads of fun, so I’ve added a little whimsy to the alert messages.
    3. I’ve got a live activity so I don’t have to unlock the phone to reset the interval when I go.
    4. Added quiet hours so it doesn’t ping me every two hours when I’m trying to sleep.
    5. Explanatory “why the hell are we doing this anyway” text. Okay, I don’t need that, but it’s useful for someone to refresh their memory, or to get an explanation of why we’re doing it if their urologist has said, “make sure you pee every two hours.”

    Running with this version a while to see what else it needs. Will look into art and design after it’s functional.

  • Not an app I thought I’d need, but here we are

    So one of the things that can happen after prostate cancer radiation treatment (and I note that I don’t think I’ve written a post about that — I suppose I was a bit distracted at the time) is that you can develop what’s called a stricture, ot narrowing, of the urethra from scar tissue forming.

    This can cause you to not completely empty your bladder when urinating, and that can cause your kidneys to back up and get stressed.

    You do not want to stress your kidneys.

    My urologist tells me, “Okay, we need to make sure you’re getting your bladder emptied as much as possible. I want you to go pee every two hours, whether you feel like you need to or not.” Which doesn’t seem like such a big deal, except that I am, as I have noted, somewhat ADHD, and if hyperfocus kicks in, I am very likely to get caught up in something for hours at a time, only dimly realizing that I’m tired/hungry/thirsty/need to pee.

    I could manually set myself alarms every two hours but that would be a pain to set up, and a pain to disable if I needed to. So I’m writing myself an app to do it.

    It pings me every two hours to remind me it’s time to go; if I do so early, it resets the next interval to two hours from that point. If I don’t respond, it keeps bugging me until I do.

    That’s the basic idea; I’m fairly sure I’ll want to add on to it (the process of writing this post reminds me that I really do want to be able to put it in vibrate-only mode when I’m, say, at a concert), but I’m going to get the basic version done and live with it for a while.

    No idea if this is common experience, but if it is, I’ll definitely put it out on the App Store.

  • Old man yells at cloud, redux

    I wrote about whether the cloud is a long-term safe option for your data a while back, and this article has brought me back around to thinking through that again.

    I think it’s important to realize that many of us (certainly I have) become heavily dependent on cloud services for extremely important things. Not “break the bank, on the streets” important but “huge sections of my memories not safe” important.

    I am not dissing cloud services just because they are cloud services. I am looking at them again in a more jaundiced light because they are not services in the sense of “there is something wrong, and I can talk to someone and sort this”.

    All cloud services hang a sword of Damocles over your head.

    If you somehow trigger their automated systems, they will deny you in a flash, and the chances that you will ever be able to resolve the problem are near zero, unless you are a major source of income for them (and we’re talking millions of dollars of income, not something a typical person-in-the-street can generate) or have friends in high places: either someone inside the walls who “knows a guy” or a large and public means of embarrassing them for doing something blatantly stupid via automation.

    For instance, there have been large numbers of automated account closings on Facebook of late. People have had their accounts closed in the process of opening them. Yes, this is completely nuts. No, Facebook is not doing anything. Why bother? They have plenty of users now. Who cares if a few hundred or a few thousand people lose access? (Hint: it’s the people who’ve had those accounts and trusted Facebook with their data.)

    There’s the guy who uploaded pictures of his kid’s rash for his doctor to check on Google — and lost his account.

    There have been a number of folks who lost access to their Apple IDs, resulting in major financial losses, and because they created the IDs long before two-factor and have forgotten their questions and answers…are just screwed.

    Shymala and I have had a lot of frustration with Google and Squarespace dropping the ball on her Google Workspace connected to a .studio domain. The workspace is still currently AWOL. And Squarespace is getting 5 Benjamins a year, so it’s not like this is a free account and they don’t have to care at all.

    What all these have in common is trusting an external entity to have their bests interests in mind.

    Unfortunately, even if you’re paying for it (see Squarespace), you cannot trust that a cloud provider will not arbitrarily decide that you have violated some rule – which they will not often reveal to you, because security – and you’re just done, with no options.

    So in this post, I’m going to try to look at what cloud providers I’m using and what I could do to mitigate risks with that provider.

    Google

    The biggest risk here is email. I have several different accounts, some quite old with a lot of archived email, and a few that are simply convenience accounts that I use to sign into things.

    I also have a Google workspace for mail at the pemungkah.com domain, with a couple accounts set up there.

    Risks:

    • Losing access to my primary GMail would break logins for a lot of things. I have used “Sign in with GMail” a lot. I would also lose a lot of archived mail.
      • A search in 1Password tells me I have 439 accounts associated with my primary GMail. 60-ish are “signs in with GMail” for that account.
    • Losing access for my secondary would inconvenience me more than anything else, mostly because I wouldn’t be able to receive password resets or change-of-email confirmations. Some would be harder than others to recover.
      • 29 accounts associated with my music GMail.
    • Losing access to the Google Workspace would be a relatively minor issue; I haven’t used it for a lot. I’d have to set up new logins for some of my streaming accounts.

    Probably the best thing would be to move my mail to a custom domain. I have several that would do as a base domain, and I could just set up mailboxes for the alternate accounts. A self-hosted mail service would be better; there seem to be some reasonable alternatives but they’d need to be evaluated. Using ProtonMail or Tuta with my own domain is a middle ground.

    Apple IDs

    Losing my Apple ID would be costly monetarily and emotionally.

    • Only 4 non-Apple accounts depend on my Apple ID, and I could live if I lost access to all of them.
    • All of my photos are in iCloud. If I lost access to my Apple ID, i could concievably permanently lose access to those. I have backed them up to Google Photos (yeah, not great either).
    • I’d lose access to my Apple Developer account.
    • My Find My would be broken, leading me to have to scramble to rehome all my devices. Stolen Device Protection would make that much harder to do, but I sure don’t want to turn that off.
    • I use Find My Boxes to track where stuff is in storage. If iCloud locks me out, I can’t access that data anymore. (I have written an unloader script that converts that data to HTML, so I’m less screwed than I might be there.)

    Microsoft ID

    I use this for very little. If it went away I could set up another one.

    GitHub

    A pain in the ass, but recoverable. I could create repositories somewhere else.

    Dropbox

    I need to more deeply investigate this. We use Dropbox a lot for archiving stuff, and sharing data for Shymala’s writing and art. If we lost access, there’d definitely be some problems — some of the original art has been sold, and it’d be impossible to get prints of it anymore.

    Plans

    I need to move my primary email away from GMail alone. If it’s coming to a custom domain, I can just move the mail somewhere else. Proton or Tuta seem best; self-hosting is probably even better but incurs a lot more work for me to do, plus the hosting costs, or doing it locally. (See below for thoughts on that.)

    Once I have the new mail, I need to change everything that uses GMail login first, then work through the 400 other accounts in batches. Some are simply “fuck you” accounts for the idiots who use my mail, and I can simply forget they’re there and remove them. The rest I’ll move to the new custom domain.

    I can’t completely get rid of my Apple ID without losing iCloud and Apple Developer access, so I’ll set up a new Apple ID for the custom domain, and then migrate to it. The purchases on the old account will have to stay there, so I’ll set up a new Family account with the new email as the primary and add the old Apple ID as a secondary. I will add the new account to my Apple Developer origanizatin, and move all the responsibilities over to the new account, then start purchasing the Apple Developer access from the new account.

    For GitHub, I’ll want to fork everything to the new account and close the old one. (GitHub may have a “move account”; if so I’ll use it). I will want to self-host a Git repository that I can push to as well.

    Self-hosting

    It’s possible to host most of this stuff on a server either at a colocation site or on a box literally in the house with Tailscale. Cohosted costs extra money, but has the advantage that I could put it at (say) Hetzner and outside the US, given the current political climate. In-house is max control and possibly a lesser ongoing investment but I’d incur a dependency on Tailscale. Going to have to work out the numbers and decide about this.

    Summary

    My heavy dependence on one GMail address and Google in general is not great. I should move the pemungkah.com domain to somewhere else (handwave) and use a custom email instead of GMail, and change everything over to that from the GMail address at a minimum.

    Dropbox is going to be an issue, and I still need to make sure I have a way to really for-sure back that up.

    Everything else depends on the GMail move, so I will be concentrating on that over the next few months.

  • Flutter experiences

    TL;DR: Flutter builds are as much fun as Java and Scala ones, and you spend more time screwing with the tools than you do getting anything done. I don’t think I’m going to switch, at least not now.

    As I’ve mentioned before on the blog, I maintain an iOS application for RadioSpiral’s online radio station. The app has worked well and successfully; the original codebase was Swift- Radio-Pro, which works as an iOS app and a MacOS one as well (I have been doing some infrastructure changes to support Azuracast, as previously documented on the blog.)

    We do have several, very polite, Android users who inquire from time to time if I’ve ported the radio station app to Android yet, and I have had to keep saying no, as the work to duplicate the app on Android looked daunting, and nobody is paying me for this. So I’ve been putting it off, knowing that I would have to learn something that runs on Android sooner or later if I wanted to do it at all.

    Randal Schwartz has been telling me for more than a year that I really should look at Dart and Flutter if I want to maintain something that works the same on both platforms, and I just didn’t have the spare time to learn it.

    Come the end of May 2023, and I found myself laid off, so I really had nothing but time. And I was going to need to update the app for IOS 16 anyway at that point (the last time I recompiled it, Xcode still accepted iOS 8 as a target!) and I figured now was as good a time as any to see if I could get it working multi-platform.

    I started looking around for a sample Flutter radio app, and found RadioSai. From the README, it basically does what I want, but has a bunch of other features that I don’t. I figured an app I could strip down was at least a reasonable place to start, so I checked it out of Github and started to work.

    Gearing up

    Setting up the infrastructure Installing Dart and Flutter was pretty easy: good old Homebrew let me brew install flutter to get those in place, and per instructions, I ran flutter doctor to check my installation. It let me know that I was missing the Android toolchain (no surprise there, since I hadn’t installed
    anything there yet). I downloaded the current Android Studio (Flamingo in my case), opened the .dmg, and copied it into /Applications as directed.

    Rerunning flutter doctor, it now told me that I didn’t have the most recent version of the command-line tools. I then fell into a bit of a rabbit hole. Some quick Googling told me that the command line tools should live inside Android Studio. I ferreted around in the application bundle and they were just Not There. I went back to the Android Studio site and downloaded them, and spent a fair amount of time trying to get sdkmanager into my PATH correctly. When I finally did, it cheerfully informed me that I had no Java SDK. So off to the OpenJDK site, and download JDK 20. (I tried a direct install via brew install, but strangely Java was still /usr/bin/java, and I decided rather than tracking down where the Homebrew Java went, I’d install my own where l could keep an eye on it.

    I downloaded the bin.tar.gz file and followed the installation instructions, adding the specified path to my PATH… and still didn’t have a working Java. Hm. Looking in the OpenJDK directory, the path was Contents, not jdk-18.0.1.jdk/Contents. I created the jdk-18.0.1 directory, moved Contents into it and had a working Java! Hurray! But even with dorking around further with the PATH, I still couldn’t get sdkmanager to update the command-line tools properly.

    Not that way, this way

    A little more Googling turned up a Stack Overflow post that told me to forget about installing the command-line tools myself, and to get Android Studio to do it. Following those instructions and checking all the right boxes, flutter doctor told me I had the command-line tools, but that I needed to accept some licenses. I ran the command to do that, and finally I had a working Flutter install!


    Almost.

    When I launched Android Studio and loaded my project, it failed with flutter.sdk not defined. This turned out to mean that I needed to add

    flutter.sdk=/opt/homebrew/Caskroom/flutter/ 3.10.5/flutter

    (the location that Homebrew had used to unpack Flutter — thank you find) to local.properties. After that, Gradle twiddled its fingers a while, and declared that the app was ready. (It did want to upgrade the build, and I let it do that.)

    Build, and…

    The option 'android.enableR8' is deprecated. 
    It was removed in version 7.0 of the
    Android Gradle plugin. 
    Please remove it from 'gradle.properties". 

    Okay, I remove it.

    /Users/joemcmahon/Code/radiosai/.dart_tool/ does not exist.

    More Googling, Stack Overflow says Run Tools > Flutter > Pub Get. Doesn’t exist. Okaaaaaay.

    There’s a command line version:

    flutter clean; flutter pub get

    Deleted dart_tool, then recreated it with package_config.json there. Right!

    Back to Android Studio, still confused about the missing menu entry, and build again. Gradle runs, downloads a ton of POMs and

    Couldn't resolve the package 'radiosai' in 'package:radiosai/audio_service/service_locator.dart'.

    Looking one level up, in :app:compileFlutterBuildDebug, Invalid depfile: /Users/joemcmahon/ Code/radiosai/.dart_tool/flutter_build/bff84666834b820d28a58a702f2c8321/ kernel_snapshot.d.

    Let’s delete those and see if that helps…yes, but still can’t resolve
    radiosai. Okay, time for a break.

    Finally, a build!

    Another Google: I wasn’t able to resolve the package because I needed to pub get again.

    Module was compiled with an incompatible version of Kotlin. 

    The binary version of its metadata is 1.8.0, expected version is 1.6.0. Another Google. One of the build Gradle files is specifying Kotlin 1.6…it’s in /android/ build.gradle. Update that to 1.8.10, build…Kotlin plugin is being loaded, good. Couple
    warnings, still going, good.

    BUILD SUCCESSFUL

    Nice! Now, how do I test this thing? Well, there’s Device Manager over on the right, that looks promising. There’s a “Pixel 3a” entry and a “run” button. What’s the worst that could happen?

    Starts up, I have a “running device” that’s a couple inches tall, on its home screen. Hm. Ah, float AND zoom. Cool. Now I realize I have no idea how to run an Android phone, and I don’t see the app.

    https://developer.android.com/studio/run/emulator…nope. Beginning to remember why I didn’t like working in Scala… Gradle upgrade recommended, okay, and now

    Namespace not specified. Please specify a namespace in the module's build.gradle. 

    Specified, still broken…googling…This is a known issue –
    https://github.com/ionic-team/capacitor/issues/6504

    If you are using Capacitor 4, do not upgrade to Gradle 8.


    Yeah, I remember why I stopped liking Scala. git reset to put everything back…

    Execution failed for task:gallery_saver:compileDebugKotlin'. 
    > compileDebugJavaWithJavac task (current target is 1.8) and 'compileDebugKotlin' task
    (current target is 17) 
    jvm target compatibility should be set to the same Java version.
    Consider using JVM toolchain: https://kotl.in/gradle/jvm/toolchain 

    Fix android/app/build.gradle so everyone thinks we’re using Java 17, which uses a different syntax, ugh.

    Fix it again. Same for the Kotlin target too.

    'compileDebugJavaWithJavac' task (current target is 1.8) and 'compileDebugKotlin' task (current target is 17) jvm target compatibility should be set to the same Java version.

    This is apparently actually Gradle 8 still lying around after the (incorrectly) recommended upgrade. Removing ~/ gradle to nuke from orbit. Also killing android/.gradle.


    [Aside: I am used to using git grep to find things, and it is just not finding them in this repo!]

    Cannot read the array length because "" is null

    WHAT.

    Apparently this means that Gradle 8 is still lurking. Yep, the rm ~/.gradle/* didn’t remove everything because of permissions. Yougoddabefuckingkiddingme. Sudo’ed it, relaunched with the fixes I made above. App runs!


    However it stops working after a bit with no reason indicating why. Let’s stop it and restart. Stop button did not stop it; had to quit Android Studio.

    Well. Okay. This is not promising, but let’s see the benefit of using Flutter; we’ll check out if the iOS side works. Seems a lot more straightforward, though I’m not doing much in Xcode. cd iOS, launch the simulator (important!), flutter run…and we get the Flutter demo project. Looks like the IOS version wasn’t brought over from the Android side. Why did you even do this.

    Do we all remember that I wanted something that worked on both platforms? I do. We don’t. Gah.

    So I’m putting Flutter aside, cleaning up the ton of disk space all this extra infrastructure took up, and will maybe come back to it another time.

    But for right now, the amount of work involved is absolutely not worth it because I’d have to write the damn thing from scratch anyway.

    Maybe I’ll run this through one of the LLMs and see if it can get me a common codebase as a starting point, but I am not sanguine.

    [Note from the future: my fellow DJ from RadioSpiral, DJ Cosmos, has written a Go/Flutter implementation that works great on Linux and Android, so I don’t have to do this anymore!]

  • So long, pemungkah@me.com

    That email address is now officially defunct.

    I checked; I created it back when I bought my iPhone 5.

    Years ago, it got leaked, and it has since been used for everything from someone in Canada’s VISA card to a bank account in Vietnam to some bozo’s Marriott account. (Hey David: thppppppppbt.)

    It got so bad that when Apple opened up creating Apple IDs with your own email, I did that, and essentially abandoned the me.com address.

    I used it for just political mail for a while, but I’ve gotten disillusioned that letting every random person running for office send me begging letters does any good, let alone giving them money. (They’re never “I did this because that’s what you sent me to Congress to do”, but “MY OPPONENT HAS MONEY! WE’RE CRYING! SEND ME MORE!” — and most of the time it’s futile anyway.) Mostly it was spam, people using it as a test email (especially screw you people: use MailHog or something. How are you going to know if the mail you’re sending looks right it it just goes somewhere you cant even read?)

    I closed it today. Only had a couple things still left from the downloads I used it for, and I can reinstall them from my primary if I want them.

    A grand experiment, but I’m not sad to never have to deal with it again.

  • OCLP experience update: back to Ventura

    I’ve been running OCLP (the Open Core Legacy Patcher) on my 2012 MacBook Pro; recently I ran softwareupdate from the command line and accidentally upgraded to Sonoma from Ventura. The experience was definitely mixed.

    It handled it mostly okay for day-to-day work. Xcode 15.4 ran fine. Where I hit a problem, though, was when I tried running Azuracast under Docker. The machine ran insanely hot, so hot that it started throwing screen glitches. Rather than burn out my GPU, I elected to downgrade to Ventura. Here’s how that went. (Spoiler: a lot of toil.)

    Getting Ventura back

    The first step was to get Ventura back on the machine, This wasn’t particularly hard; I just needed to follow the standard OCLP procedure, but install to a new partition on my internal SSD. This cut the amount of space down by about another 200GB, but went well. I was able to install and have Ventura in good shape in a couple hours.

    Retrieving the data from Sonoma

    Here’s where we started having problems.

    I had hoped that I’d be able to use Migration Assistant to bring the data back from Sonoma to Ventura, but no dice. Migration Assistant looked at the Sonoma disk, said, “nuh-uh, I ain’t downgrading” and refused to even consider mounting the disk. This meant I’d have to port everything back from that disk to the new one manually.

    My first try was to rsync it over. This failed because now I didn’t have enough space to have two copies of the data. I deleted the data from the Ventura install and tried again. This time I created a service account with admin privileges, and copied ~/Library over from Sonoma. This didn’t seem to work either; most particularly iCloud login was broken.

    Fixing the broken copy

    After thinking about it a while, I decided that the problem was probably permissions. From the service account, I wiped the Ventura copy of my account again, and copied in two steps. First, I copied ~/Library over, then chown‘ed it to my user on the Ventura disk. I logged in as myself, set up iCloud, and all was good. Now came the question of moving the data without filling the disk.

    I was able to use rsync (from the service account again), but this time I added --remove-source-files and --ignore-existing to the command. This copied only files I didn’t already have on Ventura from Sonoma, deleting them as they transferred. After this finished, I logged in to my Ventura account, iCloud was okay, and all my files were back.

    I then rebooted into the installer again, removed the Sonoma partitions, and was ready to go.

    I’m now currently running Azuracast under Docker, and having it ingest the RadioSpiral tracks from my iTunes library. It’s running warm, but not hot, and no more screen glitching. I’ll probably leave it on Ventura unless someone else running the same machine gets good performance from Sequoia.

    And I can always run Linux if all else fails.

  • Leveraging an outage to build community and consensus

    We had our first extended outage at RadioSpiral this weekend, and I’m writing about it here to point out how a production incident can help bring a team together not only technically, but as a group.

    The timeline

    On Sunday evening, about an hour before Tony Gerber’s Sunday show, RadioSpiral went offline. Normally, the AirTime installation handles playing tracks from the station library when there’s no show, and it played a track…and then stopped. Dead air.

    The station has been growing; we’ve added two new DJs, doubling the number of folks who are familiar with servers, Linux, etc. Everyone who was available (pretty much everyone but our primary sysadmin, who set everything up and who is in the UK) jumped in to try to see what was up. We were able to log in to AirTime and see that it was offline, but not why; we tried restarting the streaming service, and the server itself, but couldn’t get back online.

    We did figure out that we could connect to the master streaming port so that Tony could do his show, but after that, we were off the air for almost 12 hours, until our primary sysadmin was up, awake, and had finished his work day.

    A couple hours of investigation on his part did finally determine that LetsEncrypt had added a RewriteRule to the Airtime configuration that forced all URLs to HTTPS; unfortunately it needs HTTP for its internal APIs and that switchover broke it. Removing the rule and restarting the server got us back on line, and our very patient and faithful listeners trickled back in over the day.

    Now what?

    While we’d not been able to diagnose and fix the problem, we had been chatting in the staff channel on the RadioSpiral Discord server, and considering the larger issues.

    RadioSpiral is expected to be up 24/7, but we’re really running it more like a hobby than a business. This is reasonable, because it’s definitely not making any of us money, at least not directly. (Things like sales of albums by our DJs, etc., are their business and not part of the station’s remit.) This means that we can have situations like this one, where the station could be offline for an extended amount of time without recourse.

    Secondarily, RadioSpiral is small. We have three folks who are the core of actual station operations, and their contributions are very much siloed. If something should happen to any one of the three of us, it would currently be a scramble to replace them and could possibly end up with an extended loss of that function, whether broadcast operations, the website, or community outreach and the app.

    So we started looking at this situation, and figuring out who currently owned what, and how we could start fixing the single points of failure:

    • Station operations are on an ancient Linux release
    • We’re running an unsupported and unmaintained version of Airtime. It can’t even properly reset passwords, a major problem in an outage if someone can’t get in.
    • The MacOS/iOS app is handled by one developer; if that person becomes unavailable, the app could end up deleted from the store if it’s not maintained.
    • The website is being managed by one person, and that person becomes unavailable…well, the site will probably be fine until the next time the hosting bill isn’t paid, but if there were any issues, we’d be SOL.
    • We do have documentation, but we don’t have playbooks or process for problem solving.
    • We don’t have anywhere that is a gathering point when there’s a problem.
    • We don’t have project tracking so we can know who’s doing what, who their backup or backups are, and where things are in process.
    • We don’t have an easily-maintained central repository of documentation.

    What we’re doing

    I took point on starting to get this all organized. Fixing all of the things above is going to take time and some sustained effort to accomplish, and we’re going to want to make sure that we have everything properly set up so that we minimize the number of failure points. Having everyone onboard is critical.

    • We’re going to move operations to a newer, faster, and cheaper server running a current LTS Ubuntu. [Done.]
    • We’re going to upgrade from the old unsupported AirTime to the community-supported LibreTime. {We did better, and moved to Azuracast.]
    • We’re figuring out who could get up to speed on MacOS/iOS development and be ready to take over the app if something should happen that I couldn’t continue maintaining it. At the moment, we’re looking at setting up a process to bump the build number, rebuild with the most current Xcode, and re-release every six months or so to keep the app refreshed. Long-term we’ll need a second developer (at least) who can build and release the app, and hopefully maintain it. [There’s enough active development happening that the going idle isn’t a problem, but the second dev is still bus factor 1.]
    • We haven’t yet discussed what to do about the website; it is already a managed WordPress installation, so it should be possible to add one or more additional maintainers. [Rebekkah is still primary, but we can all get in and do things now.]
    • We are going to need to collect the docs we have somewhere that they can be maintained more easily. This could be in a shared set of Google docs, or a wiki; we’re currently leaning toward a wiki. [Wiki up on the main site.]
    • We need project tracking; there’s no need for a full-up ticketing process, at least yet. We think that Trello should do well enough for us. [We added a ticket system inside the main site; working okay so far.]

    We have set up some new Discord channels to keep this conversation open: #production-incidents, to make tracking any new problems easier, and #the-great-migration, to keep channels open as we move forward in the migration to our new setup.

    Everyone is on board and enthusiastic about getting things in better shape, which is the best one could want. It looks good for RadioSpiral’s future. Admittedly we should have done this before a failure, but we’re getting it in gear, and that’s better than ignoring it!

  • Archiving papers: a strategy

    I’m helping a friend archive a lot of notebooks and papers that they’ve accumulated over several years of writing. They’d like to be able to travel, but are a little worried that not having any backup for all this work is risky; fires, floods, and theft do happen, so even a fireproof box isn’t a guaranteed backup.

    We’ve therefore been photographing the papers, page by page, and creating a 3-2-1 backup of all of the digital photos. After some experimentation, we’ve come up with a workflow that works very well:

    • Create a Photos library that is not the primary. (She has an art business and needs to be able to use her iCloud-synced Photos library without it getting cluttered up with hundreds of photographs of pages.) This is most easily done by holding down Option and launching Photos. When the “select the library” dialog comes up, create a new one.
    • Photograph the items on a second iCloud account’s primary Photos library. This automatically syncs them to that accounts iCloud Photos.
    • On the machine where the secondary Photos library lives, log into iCloud.com with the second account.
    • On that same machine, open Photos with the non-primary library. (Hold down the option key and open Photos to allow Photos to select the non-primary Photos library.)
    • As batches of photos are taken, wait for them to sync to iCloud, then on the iCloud.com page for the second account, download the batch to the machine where the secondary library lives.
    • Create a new album in that secondary library, and drag the new batch of photos into it.
    • Put a sticker on the notebook/folder, and write in an ID (A, B, C, etc.) and the date it was photographed last. This allows active notebooks to be archived safely. (You should also add a note on the last page scanned with the date and album ID so you can cross-check.)

    Photographing the cover of the notebook/the file folder the pages are in helps make sure that you keep different batches of photos separate. If you do this, it’s much easier to keep track of which pages belong in which album, and gives a better way to track back which things are done and which aren’t.

  • leetcode day 30 – I am not friends with slices: level-order tree

    Visit the nodes of a binary tree in level order: all nodes at the root level, left-to-right, all nodes at level 1, left-to-right, etc.

    I did try to solve this with slices and boy did I have trouble. I eventually stopped trying to use them and went to a list-oriented solution with a double closure.

    /**
     * Definition for a binary tree node.
     * type TreeNode struct {
     *     Val int
     *     Left *TreeNode
     *     Right *TreeNode
     * }
     */
    
    type MyList struct {
        head *MyNode
        tail *MyNode
    }
    
    type MyNode struct {
        Val int
        Next *MyNode
    }
    
    func levelOrder(root *TreeNode) [][]int {
        // Ideally, I want to end up with an array
        // of arrays of nodes, added in left-to-right order.
        getStash, helper := createClosures()
        helper(root, 0)
        stash := getStash()
    
        out := [][]int{}
        for i := 0; i < len(stash); i++ {
            out = append(out, []int{})
            thisList := stash[i]
            for scan := thisList.head; scan != nil; scan = scan.Next {
                out[i] = append(out[i], scan.Val)
            }
        }
        return out
    }
    
    func createClosures() (func() []MyList, func(*TreeNode, int)) {
        stash := []MyList{}
    
        var helper func(*TreeNode, int)
        helper = func(root *TreeNode, level int) {
                if root == nil {
                    // Nothing to do at this level
                    return
                }
                // Current node gets stashed at the end of the list for this level.
                // (*output)[level] is the slice to append to.
                // Add new node to list at this level
                if len(stash) <= level {
                    stash = append(stash, MyList{})
                }
                
                n := &MyNode{Val: root.Val}
                if stash[level].head == nil {
                    stash[level].head = n
                    stash[level].tail = n
                } else {
                    stash[level].tail.Next = n
                    stash[level].tail = n
                }
     
                // add the left and right subtrees at the next level down
                helper(root.Left, level + 1)
                helper(root.Right, level + 1)
            }
        return func() []MyList { return stash }, helper
    }

    This solves the problem, but it’s poor on memory (8th percentile) and runtime (26%). On the other hand, the list manipulation worked correctly the first time. Let’s try to speed it up. First let’s try to remove the lists.

    /**
     * Definition for a binary tree node.
     * type TreeNode struct {
     *     Val int
     *     Left *TreeNode
     *     Right *TreeNode
     * }
     */
    
    func levelOrder(root *TreeNode) [][]int {
        // Ideally, I want to end up with an array
        // of arrays of nodes, added in left-to-right order.
        getStash, helper := createClosures()
        helper(root, 0)
        return getStash()
    }
    
    func createClosures() (func() [][]int, func(*TreeNode, int)) {
        // This will be our output structure. As we go down the
        // levels, we add more []int slices to hold the next level
        // of nodes. Because we work left to right, we always add
        // new nodes at a level to the correct slice, and they always
        // are added to the right end of the slice, giving us the
        // output data structure we want.
        stash := [][]int{}
    
        // We have to declare a named variable to be closed over for
        // a Go anonymous function to be able to call itself.
        var helper func(*TreeNode, int)
    
        // The real meat of the process.
        helper = func(root *TreeNode, level int) {
                if root == nil {
                    // Nothing to do at this level
                    return
                }
                // Current node gets stashed at the end of the slice
                //  for this level.
                // stash[level] is the slice to append to.
                if len(stash) <= level {
                    // We have never accessed this level. Add a slice
                    // so appends will work. You CANNOT just assign to
                    // stash[level], because it doesn't exist yet and
                    // you'll get an out of bounds error.
                    stash = append(stash, []int{})
                }
                // Add this node's value to the end of the list at this
                // level.
                stash[level] = append(stash[level], root.Val)
    
                // add the left and right subtrees at the next level down.
                // Because we're tracking the level, we always append to the
                // right slice in the stash, which lives outside the recursion.
                helper(root.Left, level + 1)
                helper(root.Right, level + 1)
            }
        // The two functions the main function needs to call. Stash will be
        // exactly the needed data structure when helper finishes.
        return func() [][]int { return stash }, helper
    }

    That’s a significant improvement; 71st percentile runtime, 24th percentile memory. The problem I had the first time around was not properly managing the slices. To add an element to a slice after the current end, you must use append(), and I needed to do this for each level as I got there. The code to build the output structure from the lists was the learning experience I needed to get that part right in the rewrite.

    Still definitely more comfortable manipulating data structures. Let’s try removing the closure and see how that goes; we’ll just let scoping make the stash visible to the helper.

    /**
     * Definition for a binary tree node.
     * type TreeNode struct {
     *     Val int
     *     Left *TreeNode
     *     Right *TreeNode
     * }
     */
    
    func levelOrder(root *TreeNode) [][]int {
        // Ideally, I want to end up with an array
        // of arrays of nodes, added in left-to-right order.
        stash := [][]int{}
    
    var helper func(*TreeNode, int)
        helper = func(root *TreeNode, level int) {
                if root == nil {
                    // Nothing to do at this level
                    return
                }
                // Current node gets stashed at the end of the list for this level.
                // stash[level] is the slice to append to.
                // Add new node to list at this level
                if len(stash) <= level {
                    stash = append(stash, []int{})
                }
                stash[level] = append(stash[level], root.Val)
    
                // add the left and right subtrees at the next level down
                helper(root.Left, level + 1)
                helper(root.Right, level + 1)
            }
    
        helper(root, 0)
        return stash
    }

    Interestingly, this is actually worse according to leetcode; 68th percentile now (3ms instead of 1ms). Honestly I think we’re in the noise at this point. No change in the memory usage.

    I think if I’d gotten this in an interview I’d call the second or third solution good, and the first marginal. The rewrite time from the list version to the working slice version was only a couple minutes, and I never felt like I didn’t grasp the necessities of the problem.

  • leetcode month, day 2: Fenceposted by “valid parens”

    Today’s challenge is Valid parens. And it reminded me of several of the things that I don’t particularly like in Go.

    Problem statement boils down to “here’s a string of brackets: parens, square brackets, braces. Tell us if they match properly (true) or don’t (false)”. Algorthmically this is dead simple:

    Start with an empty stack.
    for each character in the string:
      if it's an open bracket, push it.
      if its a close bracket:
        if the stack is empty, return false.
        if the top of the stack isn't the matching open, return false.
        pop the stack.
    
    When we're done, return true if the stack is empty, false otherwise.

    In Perl or Python, I’d have a stack pointer, and move it up and down an array as I pushed and popped characters by altering the stack pointer and assigning to the appropriate point in the array. If the stack pointer went below 0, I’ve gotten too many close parens, and I return false.

    This approach doesn’t work well at all with Go, because Go arrays and slices Do Not Work Like That.

    To push onto an existing array, we need to use a := append(a, new). Just assigning to an index outside of the slice’s current boundaries will panic.

    We could still use the stack pointer to move backward, but then we’d have to have more complex code to decide if we need to append or just move the pointer. (Side note: doing it that way would probably use less memory — the working version of my solution used more memory than 90% of the other solutions). Instead, we just use the slice notation to pop the stack instead, with a := a[:len(a)-1].

    My original iterations with a real stack pointer failed miserably because I wasn’t properly tracking the length of the array, and was perpetually getting the stack pointer position wrong, causing the code to panic. It was only after completely discarding the stack pointer that I got it to finally work.

    Items of note:

    • I remembered I’d need to use range() to iterate over the string, but forgot that I would have to string() the characters I was getting so they were strings instead of runes. I wasted a good chunk of time trying to mess with runes before I realized that I needed string() instead. Runes are considerably more second-class citizens.
    • Still running afoul of using make() properly. Finally figured out the syntax to create an empty string array, but it took some Googling to get it.
    • I decided to make the code less complex by creating a pairs map that mapped left brackets to right ones. This meant I could check pairs[left] == right for whether I’d matched the left and right brackets. It also meant that if we ever added more bracket pairs in the future, it’d be be easier to implement them.
    • I got fenceposted by a[len(a)-1] accessing the last element, and a[:len([a)-1] dropping the last element. I naively expected that since a[len(a)-1] accessed the last element, I’d want a[:len(a)-2] to drop that last element, but that takes one too many, running me into another fencepost. Yes, it’s good that it’s symmetric, and now I’ll remember, but I definitely had to look it up to figure out both of them.
    • Forgot the check for “did we close all the opens”. I probably would not have missed it if I was making my own test cases, but it wasn’t in the tests. Note to self: watch out for poor initial test cases. There is an opportunity to add more, I think?

    So how’d I do overall? We saw I was in the bottom 10% in memory usage, boo, but runtime? “Beats 100.00% of users with Go”.

    I’ll definitely take that as a win, but it’s definitely obvious I need to keep practicing and getting these idioms back under my belt.

    func isValid(s string) bool {
        // We'll run through the string, doing the following:
        // - if we see an open bracket, we stack it.
        // - if we see a close bracket, we pop the stack. If the
        //   brackets match, we continue, otherwise we fail.
        // - when we run out of brackets, the stack must be empty or we fail.
    
        stack := make([]string,0)
    
        pairs := map[string]string{
            "(": ")", 
            "[": "]", 
            "{": "}",
        }
    
        for i := range s {
            char := string(s[i])
           _, ok := pairs[char]
           if ok {
               // left, push it
               stack = append(stack, char)
           } else {
               // right, stack empty?
               if (len(stack) == 0) {
                   fmt.Println("pop of empty stack")
                   return false
               }
               // Not empty. match?
               if (pairs[stack[len(stack)-1]] != char) {
                   fmt.Println ("mismatch")
                   // mismatched bracket, fail
                   return false
               }
               // Match. Pop stack.
               stack = stack[:len(stack)-1]
           }
        }
        // Check for "did we close all the open brackets".
        return len(stack) == 0
    }

    That’s one for today, and I need to clean the house, so I’ll call it a day.