Building TRB on a 2022 Vintage Musl Gentoo

January 30th, 2022

I've been working on a new musl-based Gentoo build for my ThinkPad X1 Carbon (Gen 6) laptop. I recently popped in a new 2TB Samsung 970 EVO NVMe SSD since the original 512GB SSD was feeling a little cramped with the weight of Bitcoin's blockchain now approaching 400GB. Unlike my last Gentoo build on an FX-8350-based desktop, with this one I am documenting every step and pitfall along the way so that others new to TRB and V (and perhaps even Gentoo, or any flavor of Linux for that matter) can follow a set of instructions and have their own working vtron and TRB, on top of a reasonably sane Linux environment that is perfectly usable as a daily driver. As a prerequisite for publishing this guide I had to be sure that I could in fact build TRB on this system, otherwise the rest of the steps leading up to the TRB build would have been, for the most part, pointless. Well, I am now happy to report that (with only minimal patching) building TRB on a stock 2022 vintage Gentoo is indeed possible.

Building TRB with the System's Compiler and Libraries

To start, I grabbed all the patches up to and including jfw's system compiler patch. While I've had success building TRB via the 'Rotor' method on older Gentoo builds, namely asciilifeform's Dulap, my most recent attempt on a 2021 glibc-based Gentoo got stuck on this requirement for the m4 dependency. Additionally, jfw's system compiler is much faster, since there are a lot fewer steps, so I figured if I could get it to work it would be preferable.

Once I had all the patches and signatures I pressed the source and entered the new directory. From there I made my first attempt at make ONLINE=1.

Using the Right Version of Python

The first hiccup I encountered was with building the Boost dependency. It wasn't immediately apparent what the problem was but I remembered whaack had done a writeup of his experience using the system compiler build method so I checked his notes. Sure enough there was something in there about the Boost build failing, only he happened to also catch that it had something to do with Python. He was on 2.6 and the fix for him was to use 2.7. Out of the box my Gentoo came with Python 3.9.91 so I first installed 2.7 from Portage, along with virtualenv, and then set the virtual environment to target Python 2.7.


sudo emerge -av =dev-lang/python-2.7.18_p13
sudo emerge -av dev-python/virtualenv
virtualenv --python=python2.7 .env
source .env/bin/activate

With my Python virtual environment set I ran make again. This time Boost compiled successfully and the build continued until it hit the next snag with Berkley DB.

Patching Berkley DB to Build with Newer Versions of GCC

Compiling Berkley DB failed with an error that looked like:


../dist/../dbinc/atomic.h:179:19: error: definition of 'int __atomic_compare_exchange(db_atomic_t*, atomic_value_t, atomic_value_t)' ambiguates built-in declaration 'bool __atomic_compare_exchange(long unsigned int, volatile void*, void*, void*, int, int)'
  179 | static inline int __atomic_compare_exchange(
      |                   ^~~~~~~~~~~~~~~~~~~~~~~~~

I googled for some clues and found that the problem is fairly common, and the solution is straightforward. The issue is that some versions of GCC2 have their own __atomic_compare_exchange function that conflicts with that of BDB's. The solution is to rename BDB's to something else, so I added the patch and extra line to the build/Makefile and ran make again. This time there were no issues with BDB and I ran into the final issue preventing a successful TRB build.

A Small Adjustment to Boost's API

It's likely this won't even be an issue for you unless you've already installed a more recent version of Boost3. I got this error inside src/net.cpp:


../src/net.cpp:35:1: error: reference to 'array' is ambiguous
   35 | array vnThreadsRunning;
      | ^~~~~

Luckily this was also something that others had run into and tracked down to an API change in later versions of Boost. The fix is simply to update the offending line, like so:


-array vnThreadsRunning;
+boost::array vnThreadsRunning;

With this change in place the entire build finally succeeded and I was left with a working bitcoind inside of build/. I moved this to a more generic location on my machine, ran it, and now my new TRB is up and eating blocks.

Some Remaining Questions

There was one part of this process that didn't quite add up: if I had to go through the trouble of building the specified Boost 1.52.0 then why did the build at some point use my local 1.77.0 copy at /usr/lib? I decided to try the build again, this time removing all pointers to the Boost library in deps/Makefile and build/Makefile. This also worked for me4 and was the fastest I had ever seen TRB compile. I tested the resulting binary and it continued to eat blocks where the other left off just fine.

The two binaries were of different weights though, 31MB and 33MB. Running ldd showed some differences in their linked libraries:

Built with the specified Boost 1.52.0 present

$ ldd bitcoind
/lib/ld-musl-x86_64.so.1 (0x7ff899970000)
libstdc++.so.6 => /usr/lib/gcc/x86_64-gentoo-linux-musl/11.2.0/libstdc++.so.6 (0x7ff899344000)
libgcc_s.so.1 => /usr/lib/gcc/x86_64-gentoo-linux-musl/11.2.0/libgcc_s.so.1 (0x7ff89932b000)
libc.so => /lib/ld-musl-x86_64.so.1 (0x7ff899970000)

Built with only the local Boost 1.77.0

$ ldd bitcoind
/lib/ld-musl-x86_64.so.1 (0x7f43030db000)
libboost_system.so.1.77.0 => /usr/lib/libboost_system.so.1.77.0 (0x7f4302d0e000)
libboost_filesystem.so.1.77.0 => /usr/lib/libboost_filesystem.so.1.77.0 (0x7f4302cec000)
libboost_program_options.so.1.77.0 => /usr/lib/libboost_program_options.so.1.77.0 (0x7f4302c9c000)
libboost_thread.so.1.77.0 => /usr/lib/libboost_thread.so.1.77.0 (0x7f4302c7d000)
libstdc++.so.6 => /usr/lib/gcc/x86_64-gentoo-linux-musl/11.2.0/libstdc++.so.6 (0x7f4302a76000)
libgcc_s.so.1 => /usr/lib/gcc/x86_64-gentoo-linux-musl/11.2.0/libgcc_s.so.1 (0x7f4302a5d000)
libc.so => /lib/ld-musl-x86_64.so.1 (0x7f43030db000)
libboost_atomic.so.1.77.0 => /usr/lib/libboost_atomic.so.1.77.0 (0x7f4302a53000)

I don't understand why the two binaries have different library links when they appeared to use the same local copy of Boost at some point during the build,5 but I'm content for now with having found two new ways of buliding a working TRB on a 2022 Musl Gentoo.

The Unsigned Patch

I figured since I'd already done the work of patching the build to work on my heathen Gentoo configuration I'd share the complete set of changes in case anyone else wants to run TRB on a similar setup.

bitcoin_heathen_linux_build_fixes.vpatch

  1. Modern Gentoos now ship with Python 3, and eselect python no longer lets you select Python 2.7 as the system default Python. []
  2. I'm not sure why I only bumped into this now, considering the function was first added to GCC all the way back at version 4.7.0. If any GCC experts can write in and let me know what I'm missing I'd much appreciate it, I was using GCC 11.2.0 fwiw. []
  3. As I apparently did when I installed AbiWord. There are also a number of other packages that require Boost so it's possible you have it installed already. []
  4. And no longer required a specific version of Python. []
  5. At least that was my hunch, as indicated by them barfing on the same line in src/net.cpp. []

New Logotoron Patches: Bug Fixes and Enhancements

January 22nd, 2022

I've put together another small set of patches for asciilifeform's logotron1. Some of the changes are small adjustments I've been running on my copy for a while now, a few are necessary updates for working with the Pest network, and lastly there's a more critical fix for a bug that was discovered recently by a member in #asciilifeform. I'll start with that one because it's the most entertaining.

Bot Recursion

The fun starts here2, when verisimilitude (wot: verisimilitude) figured out how to exploit a bug in the log bot to cause an infinite loop of line echos between two bots sitting in the same channel. The bot was easily patched by simply returning early in eat_logline, after saving the line, if the line came from another bot. The list of bots already existed as a knob in the config but was previously only used for muting the colors of bot log lines in the www display.

Another small change in this patch is the is_pest flag in the config, which the bot now uses to determine which regex to use when parsing log lines. This was useful for me now, since I have a bot each in both dulapnet and pestnet, and will be useful for others who want to also run a bot in a pestnet.

1 diff -uNr a/logotron/bot.py b/logotron/bot.py
2 --- a/logotron/bot.py 048fcb73260b4b85459e1e8ca4529df95a1e4d6898b74a8259c38a690aeb0840cf77fef9fd5f16445287f0d4091e18ddf7e2da478e9cb09db157f16eeb4c85ec
3 +++ b/logotron/bot.py fa93d94c684695b4ac20f57b9580d2e1faf9e82e83145eaca7a9ff2d1130d5e296c0e1007d771ba7cd17b4dde33d4da2c6975af8d9e3a29412dcd3f5d013f25e
4 @@ -14,7 +14,7 @@
5 ##############################################################################
6
7 # Version. If changing this program, always set this to same # as in MANIFEST
8 -Ver = 689331
9 +Ver = 719633
10
11 ## As of version 689331, this program will NOT work with Freenode !!!
12
13 @@ -64,12 +64,14 @@
14 TX_Delay = float(cfg.get("tcp", "t_delay"))
15 Servers = [x.strip() for x in cfg.get("irc", "servers").split(',')]
16 Port = int(cfg.get("irc", "port"))
17 + Is_Pest = int(cfg.get("irc", "is_pest"))
18 Nick = cfg.get("irc", "nick")
19 Pass = cfg.get("irc", "pass")
20 Channels = [x.strip() for x in cfg.get("irc", "chans").split(',')]
21 SkipInitLn = int(cfg.get("irc", "skip_init_lines"))
22 Discon_TO = int(cfg.get("irc", "disc_t"))
23 Prefix = cfg.get("control", "prefix")
24 + Bots = [x.strip() for x in cfg.get("control", "bots").split(',')]3
25 # DBism:
26 DB_Name = cfg.get("db", "db_name")
27 DB_User = cfg.get("db", "db_user")
28 @@ -214,8 +216,11 @@
29 save_line(datetime.now(), channel, Nick, False, message)
30
31
32 -# Standard incoming IRC line (excludes fleanode liquishit, etc)
33 -irc_line_re = re.compile("""^:([^!]+)\!\S+\s+PRIVMSG\s+\#(\S+)\s+\:(.*)""")
34 +# Standard incoming IRC (or PestNet) line (excludes fleanode liquishit, etc)
35 +if (Is_Pest):
36 + irc_line_re = re.compile("""^:(\S+)\s+PRIVMSG\s+\#(\S+)\s+\:(.*)""")
37 +else:
38 + irc_line_re = re.compile("""^:([^!]+)\!\S+\s+PRIVMSG\s+\#(\S+)\s+\:(.*)""")
39
40 # The '#' prevents interaction via PM, this is not a PM-able bot.
41
42 @@ -527,6 +532,10 @@
43 # First, add the line to the log:
44 save_line(datetime.now(), chan, user, action, text)
45
46 + # End here if the line came from another bot
47 + if user in Bots:
48 + return
49 +
50 # Then, see if the line was a command for this bot:
51 if text.startswith(Prefix):
52 cmd = text.partition(Prefix)[2].strip()
53 diff -uNr a/logotron/nsabot.conf b/logotron/nsabot.conf
54 --- a/logotron/nsabot.conf b2117eaa3fbedff592b133635a84997b236d781936c9e8473d771ad67ab4e20a4a36062c9449b4ac0ee0fc7cd4c2bced405105a250378bd3a6a5658de05c3a30
55 +++ b/logotron/nsabot.conf ad5f570234ff7f7afa4e2d43669c36e502c820a6645c5f241a255e608ce897133ca31caf2d26269204d63ed7b49c793589987581aba503dad6c3a64a502849cd
56 @@ -6,6 +6,8 @@
57 [irc]
58 servers = irc.dulap.xyz
59 port = 6667
60 +# If the bot is on a Pest network, set to 1, otherwise 0
61 +is_pest = 0
62
63 # Bot's nick (change to yours, as with all knobs)
64 nick = snsabot
65 @@ -40,7 +42,10 @@
66
67 [control]
68 # Command Trigger for IRC bot
69 -prefix = !q
70 +prefix = YOUR_BOT_TRIGGER
71 +
72 +# Other people's bots (for www colouration and bot rebellion suppression)
73 +bots = dulapbot, bitbot, bitdashbot, gribble, atcbot, punkbot, []bot, assbot, a111, deedbot, deedBot, deedbot-, feedbot, auctionbot, lobbesbot, snsabot, watchglass, trbexplorer, lekythion, sourcerer, ossabot, ericbot, sonofawitch, btcinfobot, BusyBot, drunkbot, spykedbot, pehbot, BingoBot, pokarBot, scoopbot, scoopbot_revived, ozbot, mpexbot
74
75 [logotron]
76 # The current era.
77 @@ -67,9 +72,6 @@
78
79 css_file = classic.css
80
81 -# Other people's bots (for colouration strictly)
82 -bots = gribble, atcbot, punkbot, []bot, assbot, a111, deedbot, deedBot, deedbot-, feedbot, auctionbot, lobbesbot, snsabot, watchglass, trbexplorer, lekythion, sourcerer, ossabot, ericbot, sonofawitch, btcinfobot, BusyBot, drunkbot, spykedbot, pehbot, oglafbot, BingoBot, pokarBot, scoopbot, scoopbot_revived, ozbot, mpexbot
83 -
84 # Days of inactivity after which chan is hidden by default
85 days_hide = 14
86
87 diff -uNr a/logotron/reader.py b/logotron/reader.py
88 --- a/logotron/reader.py c249c7a987199fd5e0356b13c36401654b4df837e238c8c91c10c3a988b91ba6a19c908f7e33298e559ef40b058e6c67ec6e9da4bb8539c8153aa6cbf17bbabc
89 +++ b/logotron/reader.py 3b02f186daaee1b4fe93a53324fbd3c09924538f15d607c01cdb1381434ed9155671d5f239065ed431630ce3c6585c14dfcd8f3443a56189300a10d6f6288ee3
90 @@ -39,7 +39,7 @@
91 Nick = cfg.get("irc", "nick")
92 Channels = [x.strip() for x in cfg.get("irc", "chans").split(',')]
93 OldChans = [x.strip() for x in cfg.get("irc", "oldchans").split(',')]
94 - Bots = [x.strip() for x in cfg.get("logotron", "bots").split(',')]
95 + Bots = [x.strip() for x in cfg.get("control", "bots").split(',')]
96 Bots.append(Nick) # Add our own bot to the bot list
97 # DBism:
98 DB_Name = cfg.get("db", "db_name")

Postgres Auto-Reconnect

This patch just formalizes the fix I added to my bot to deal with the recurring postgres connection timeouts4. Despite only 50% of logotron operators5 having this problem I still feel it's useful to have in the main branch since it adds a layer of robustness that wasn't previously there.

Minor Tweaks and Fixes to the Web View

This last patch contains a small set of fixes/tweaks that I've been running on my copy for a while now. From the release notes:

  • Fix the target attribute on links. Hyperlinks were being rendered with target="\'_blank\'" instead of target="_blank", resulting in links opening in the same new tab rather than each in its own new tab.
  • Reduce "last active" timestamp resolution in the channel selector menu. Channels last active more than a day ago won't show minutes and channels last active > 100 days ago won't show hours. E.g. "1d 5h ago" instead of "1d 5h 24m ago" and "100d ago" instead of "100d 12h 14m ago".
  • Applicable to pestnet channels, hide hearsay annotations in the normal view. The full handle, including the hearsay annotation, can now be seen on hover as part of the title text.
  • Fix a bug in classic.css, introduced in 700821, which broke search term highlighting in the search results page.

And the prettyprinted diff for convenience:

1 diff -uNr a/logotron/reader.py b/logotron/reader.py
2 --- a/logotron/reader.py 3b02f186daaee1b4fe93a53324fbd3c09924538f15d607c01cdb1381434ed9155671d5f239065ed431630ce3c6585c14dfcd8f3443a56189300a10d6f6288ee3
3 +++ b/logotron/reader.py 3447a168ff1b14190b284952a9f4cd7619cae304f1a61169463bf677db6f3898d4a354fd8154b46ba59cb8adea177d97108f9e96d83eda33c48fa27ac9623b13
4 @@ -185,9 +185,9 @@
5
6 if days != 0:
7 last_time_txt += '%dd ' % days
8 - if hours != 0:
9 + if hours != 0 and days < 100:
10 last_time_txt += '%dh ' % hours
11 - if minutes != 0:
12 + if minutes != 0 and days == 0:
13 last_time_txt += '%dm' % minutes
14
15 last_time_url = "{0}{1}{2}/{3}#{4}".format(
16 @@ -265,11 +265,11 @@
17
18 # Format ordinary links:
19 payload = re.sub(stdlinks_re,
20 - r'<a href="\1" target=\'_blank\'>\1</a>', payload)
21 + r'<a href="\1" target="_blank">\1</a>', payload)
22
23 # Now also format [link][text] links :
24 payload = re.sub(boxlinks_re,
25 - r'<a href="\1" target=\'_blank\'>\2</a>', payload)
26 + r'<a href="\1" target="_blank">\2</a>', payload)
27
28 # For ancient logs strictly: substitute orig. link with our logger :
29 if l['era'] < 3:
30 @@ -300,26 +300,30 @@
31 speaker = l['speaker']
32 separator = ":"
33
34 + # temporary hack to remove pest hearsay annotations
35 + speaker_short = re.split('\\[.*?\\]', speaker)[0]
36 +
37 if showchan:
38 - speaker = '<small>(' + l['chan'] + ')</small> ' + speaker
39 + speaker_short = '<small>(' + l['chan'] + ')</small> ' + speaker_short
40
41 # If 'action', annotate:
42 if l['self']:
43 separator = ""
44 payload = "<i>" + payload + "</i>"
45 - speaker = "<i>" + speaker + "</i>"
46 + speaker_short = "<i>" + speaker_short + "</i>"
47
48 # HTMLize the given line :
49 s = ("<div id='{0}' class='logline {6}{5}'>"
50 - "<a class='nick' title='{2}'"
51 - " href=\"{3}\">{1}</a>{7} {4}</div>").format(l['idx'],
52 + "<a class='nick' title='{1} @ {2}'"
53 + " href=\"{3}\">{8}</a>{7} {4}</div>").format(l['idx'],
54 speaker,
55 l['t'],
56 line_url(l),
57 payload,
58 bot,
59 dclass,
60 - separator)
61 + separator,
62 + speaker_short)
63 return s
64
65 # Make above callable from inside htm templater:
66 diff -uNr a/logotron/static/classic.css b/logotron/static/classic.css
67 --- a/logotron/static/classic.css 15af7c20d831947a9333ceb510fa80acb38d983601c8d05c7cb21d5d06829a3ef7feaac18821b614c7fca148651f4849c98dd50455cafa30313a79e697975893
68 +++ b/logotron/static/classic.css 091c56349c022f5616f0b9b6e95d6e7bf4420ef4e40b10587756ffc342d580ef8684104c7fb69347fd4e07e8ca37d541e5ac7841219fa8856ceaa7246f422a73
69 @@ -98,6 +98,7 @@
70 float:right;
71 }
72
73 -.loglines div.highlight {
74 +.loglines div.highlight,
75 +.loglines span.highlight {
76 background: yellow;
77 }

Patches and Signatures

fix_bot_recursion.kv.vpatch (sig)
add_pg_reconnect.kv.vpatch (sig)
minor_reader_tweaks.kv.vpatch (sig)

  1. Also available here. []
  2. If you click on some of the logs.bitdash.io/asciilifeform links and notice they don't take you to the same log line that the bot echoed at the time it's because my db was three lines out of sync from when I inadvertently broke my bot with a system clock update. I have since added the missing lines but as a result any logs.bitdash.io/asciilifeform references during that period will be off by three lines. Check bitbot's echo to see what was the intended reference at the time. []
  3. As part of this patch I moved the bots config line from underneath logotron to control, as this seemed more appropriate now that it controls bot behavior rather than just CSS classes in the display. []
  4. Which has been working great. I've not had to restart the bot once because of a lost postgres connection and I'm no longer afraid of using my own logotron's search bar out of concern that it'll cause a long query and disconnect the bot. []
  5. That 50% being me. As far as I know asciilifeform and I are the only two people currently running this particular logotoron, and his postgres seems to behave better than mine. []

Adding Postgres Auto-Reconnect to the Logotron's IRC Bot

January 6th, 2022

I've been using asciilifeform's logotron to power my IRC logger for a while now. The setup consists of an IRC bot and a Flask-based web app, both connected to a Postgres database. I run it on a simple Rockchip machine alongside my Bitcoin network crawler, which also uses Postgres. For some reason every couple of weeks1 the bot would lose its DB connection and have to be manually restarted2 in order to continue eating log lines. Same with the crawler. Potentially there's a performance issue somewhere, causing the connection to timeout and close in some cases. I know it's not the logotron itself, since asciilifeform ran the same code on the same box for almost a year and never had the DB connection issue, but perhaps both apps sharing the same small RK is just too much for it to handle at times.

In any case, it was a problem for me, as well as others—and I figured it couldn't hurt for the bot to have auto-db-reconnect functionality—so I came up with a small patch to remedy the issue. Below is the tidied up version of what I currently have running.

1 @@ -72,6 +72,8 @@
2 DB_Name = cfg.get("db", "db_name")
3 DB_User = cfg.get("db", "db_user")
4 DB_DEBUG = cfg.get("db", "db_debug")
5 + DB_Reconn_Tries = int(cfg.get("db", "db_reconnect_max_tries"))
6 + DB_Reconn_Delay = int(cfg.get("db", "db_reconnect_delay"))
7 # Logism:
8 Base_URL = cfg.get("logotron", "base_url")
9 App_Root = cfg.get("logotron", "app_root")
10 @@ -85,21 +87,54 @@
11
12 ##############################################################################
13
14 # Connect to the given DB
15 -try:
16 - db = psycopg2.connect("dbname=%s user=%s" % (DB_Name, DB_User))
17 -except Exception:
18 - print "Could not connect to DB!"
19 - logging.error("Could not connect to DB!")
20 - exit(1)
21 -else:
22 - logging.info("Connected to DB!")
23 +db = None
24 +
25 +def conn_db():
26 + global db
27 +
28 + tries = DB_Reconn_Tries
29 +
30 + while True:
31 + # Connect to the given DB
32 + try:
33 + db = psycopg2.connect("dbname=%s user=%s" % (DB_Name, DB_User))
34 + except Exception:
35 + print "Could not connect to DB!"
36 + logging.error("Could not connect to DB!")
37 + if tries > 0 or DB_Reconn_Tries == -1:
38 + tries = tries - 1
39 + time.sleep(DB_Reconn_Delay)
40 + continue
41 + else:
42 + exit(1)
43 + else:
44 + logging.info("Connected to DB!")
45 + break
46 +
47 +conn_db()
48
49 ##############################################################################
50
51 def close_db():
52 db.close()
53
54 +def ensure_db_is_alive():
55 + # Ping the db to ensure it's alive and connected
56 + logging.debug("Checking DB connection status...")
57 + try:
58 + cur = db.cursor()
59 + cur.execute('SELECT 1')
60 + except (psycopg2.OperationalError, psycopg2.InterfaceError) as e:
61 + pass
62 +
63 + # If connection is alive db.closed will equal 0
64 + if db.closed == 0:
65 + return True
66 +
67 + # Otherwise, attempt to reconnect
68 + logging.debug("No DB Connection!")
69 + conn_db()
70 +
71 def exec_db(query, args=()):
72 cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
73 if (DB_DEBUG): logging.debug("query: '{0}'".format(query))
74 @@ -491,6 +526,8 @@
75 def save_line(time, chan, speaker, action, payload):
76 ## Put in DB:
77 try:
78 + ensure_db_is_alive()
79 +
80 # Get index of THIS new line to be saved
81 last_idx = query_db(
82 '''select idx from loglines where chan=%s

With this running locally I was able to bring down the DB, send a few IRC messages, restart the DB, and have the messages that were sent while the DB was offline recorded once the DB came back up. I'm going to give it a few weeks in production to see how it fares. If it does indeed solve the issue (and not cause any others) I'll package it as a vpatch and publish it here along with the rest of the logotron tree.

  1. Sometimes longer, but also sometimes much shorter. I suspect it has to do with the usage/load of the sites. []
  2. This happened often enough that I even wrote a small script to pull from asciilifeform's channel logs when mine fell behind:

    #!/bin/bash
    
    batch_size=500
    
    start_line=$1
    end_line=$2
    chan_url=$3
    results_file=$4
    
    if [ $1 -ge $2 ]; then
            echo "end line must be greater than the start line"
            exit 1
    fi
    
    num_lines=$(( $end_line - $start_line ))
    num_batches=$(( $num_lines / ($batch_size + 1) + 1 ))
    last_batch_size=$(( $num_lines % ($batch_size + 1) ))
    
    echo "starting at ${start_line}, ending at ${end_line}, for a total of ${num_lines} lines"
    echo "will download from ${chan_url}?istart=${start_line}&iend=${end_line} in ${num_batches} batch(es). last batch will be ${last_batch_size}"
    
    batch=1
    while [ $start_line -lt $end_line ]; do
            if [ $batch -eq $num_batches ]; then
                    cur_end=$(( $start_line + $last_batch_size ))
            else
                    cur_end=$(( $start_line + $batch_size ))
            fi
    
            echo "pulling ${start_line} - ${cur_end}"
            curl -s "${chan_url}?istart=${start_line}&iend=${cur_end}" >> $results_file
    
            start_line=$(( $cur_end + 1 ))
            batch=$(( batch + 1 ))
    done
    

    []