Monday, October 14, 2024

Compiling Software that Uses the GNU Build System (autoconf, automake, etc.)

It used to be that almost all open-source software for Linux used the GNU build system. Anytime you saw that ubiquitous 'configure' script in your source code directory, you knew what you were in for. Recently, though, it seems that use of autoconf/automake/etc. has declined, and I'm seeing more use of cmake, ninja-build, and other newer tools.

Now, that doesn't mean that these tools have completely disappeared, like imake seems to have. And, because of this, it seems worthwhile to write up a quick guide on what to do, as a builder-of-software, when you see that 'configure.ac' file, in a source code repository. I'm not going to go into depth, here, so if you would like to read more, you can check out Gentoo Linux's excellent overview.

I'm going to use the diskfit tool as an example, since I ran across it, and went through the process, earlier today. When you clone the repository from GitHub, you see this collection of files:

$ git clone https://github.com/velnias75/diskfit.git
Cloning into 'diskfit'...
remote: Enumerating objects: 1647, done.
remote: Total 1647 (delta 0), reused 0 (delta 0), pack-reused 1647 (from 1)
Receiving objects: 100% (1647/1647), 340.88 KiB | 4.94 MiB/s, done.
Resolving deltas: 100% (1201/1201), done.
$ cd diskfit
$ ls -l
total 56
-rw-r--r--. 1 ajacocks ajacocks  3456 Oct 14 14:22 configure.ac
-rw-r--r--. 1 ajacocks ajacocks 35147 Oct 14 14:22 COPYING
-rw-r--r--. 1 ajacocks ajacocks   255 Oct 14 14:22 diskfit.bc.in
-rw-r--r--. 1 ajacocks ajacocks   140 Oct 14 14:22 diskfitrc
-rw-r--r--. 1 ajacocks ajacocks   484 Oct 14 14:22 Makefile.am
-rw-r--r--. 1 ajacocks ajacocks   826 Oct 14 14:22 README.md
drwxr-xr-x. 4 ajacocks ajacocks   103 Oct 14 14:22 src

You'll notice that there is a 'configure.ac' file, but not a 'configure' script. This means that we need to build it ourselves. The process is actually very easy, but it was a real pain to find a good explanation. I did finally find one, on stackoverflow, which explains the process very well. Just in case this disappears, I'll repeat it, here:

In an autoconf/automake/libtool project you need to run:

  • libtoolize: this copies/links a few support scripts, including ltmain.sh (which is the main component of libtool).
  • aclocal: this looks up all m4 macros that your configure script will need, and make a local copy for easier access.
  • autoheader: optional, if you want to use config.h/AC_CONFIG_HEADERS, otherwise all the test result macros will be inlined when you call the compiler.
  • autoconf: to expand all the macros used by configure.ac into the configure script.
  • automake: to convert all the Makefile.am into Makefile.in templates. You probably want to invoke this with --add-missing so additional support scripts can be linked/copied to your project (such as compile, missing, depcomp, test-driver, etc).

Don't worry about running each tool. Just invoke autoreconf -i and it'll run the tools that are needed. Add -v if you want to see what tools is being executed. To avoid mistakes, just put a script like this at the root of your project:

#!/bin/bash -x
mkdir -p m4
exec autoreconf --install "$@"

Users that checkout/clone the project directly from the source repository will need to run this ./bootstrap script at least once. This is not needed if the user got a tarball distribution.

Automake can take fairly good care of itself; it'll re-invoke the above tools when needed, when you run make. But if you generate a broken Makefile, you'll need to invoke ./bootstrap and ./configure again to generate new Makefiles.

This is a far better explanation than I could have given. Thanks, DanielKO!

To continue our build process, we just need to do what DanielKO said in his snippet:

$ mkdir -p m4
$ autoreconf --install
libtoolize: putting auxiliary files in '.'.
libtoolize: copying file './ltmain.sh'
libtoolize: putting macros in AC_CONFIG_MACRO_DIRS, 'm4'.
libtoolize: copying file 'm4/libtool.m4'
libtoolize: copying file 'm4/ltoptions.m4'
libtoolize: copying file 'm4/ltsugar.m4'
libtoolize: copying file 'm4/ltversion.m4'
libtoolize: copying file 'm4/lt~obsolete.m4'
configure.ac:19: warning: The macro `AC_PROG_CC_C99' is obsolete.
configure.ac:19: You should run autoupdate.
./lib/autoconf/c.m4:1659: AC_PROG_CC_C99 is expanded from...
configure.ac:19: the top level
configure.ac:103: warning: AC_OUTPUT should be used without arguments.
configure.ac:103: You should run autoupdate.
configure.ac:9: installing './compile'
configure.ac:9: installing './config.guess'
configure.ac:9: installing './config.sub'
configure.ac:10: installing './install-sh'
configure.ac:10: installing './missing'
src/Makefile.am: installing './depcomp'

You will now see that 'autoreconf' has created a number of new files, in particular the 'configure' script that we will need to build the program: 

$ ls -l
total 1132
-rw-r--r--. 1 ajacocks ajacocks  70520 Oct 14 14:37 aclocal.m4
drwxr-xr-x. 2 ajacocks ajacocks    150 Oct 14 14:37 autom4te.cache
-rwxr-xr-x. 1 ajacocks ajacocks   7400 Oct 14 14:37 compile
-rwxr-xr-x. 1 ajacocks ajacocks  49939 Oct 14 14:37 config.guess
-rw-r--r--. 1 ajacocks ajacocks   4070 Oct 14 14:37 config.h.in
-rwxr-xr-x. 1 ajacocks ajacocks  35796 Oct 14 14:37 config.sub
-rwxr-xr-x. 1 ajacocks ajacocks 506842 Oct 14 14:37 configure
-rw-r--r--. 1 ajacocks ajacocks   3456 Oct 14 14:22 configure.ac
-rw-r--r--. 1 ajacocks ajacocks  35147 Oct 14 14:22 COPYING
-rwxr-xr-x. 1 ajacocks ajacocks  23568 Oct 14 14:37 depcomp
-rw-r--r--. 1 ajacocks ajacocks    255 Oct 14 14:22 diskfit.bc.in
-rw-r--r--. 1 ajacocks ajacocks    140 Oct 14 14:22 diskfitrc
-rwxr-xr-x. 1 ajacocks ajacocks  15358 Oct 14 14:37 install-sh
-rw-r--r--. 1 ajacocks ajacocks 332808 Oct 14 14:37 ltmain.sh
drwxr-xr-x. 2 ajacocks ajacocks    104 Oct 14 14:37 m4
-rw-r--r--. 1 ajacocks ajacocks    484 Oct 14 14:22 Makefile.am
-rw-r--r--. 1 ajacocks ajacocks  31261 Oct 14 14:37 Makefile.in
-rwxr-xr-x. 1 ajacocks ajacocks   6878 Oct 14 14:37 missing
-rw-r--r--. 1 ajacocks ajacocks    826 Oct 14 14:22 README.md
drwxr-xr-x. 4 ajacocks ajacocks    122 Oct 14 14:37 src

Now that we have what we need, we can execute 'configure' to build the Makefiles needed to successfully compile 'diskfit':

$ ./configure --prefix=/opt/diskfit
checking build system type... x86_64-pc-linux-gnu
checking host system type... x86_64-pc-linux-gnu
checking how to print strings... printf
checking for gcc... gcc
(trimmed for brevity)
config.status: creating src/python/qdiskfit/util/Makefile
config.status: creating config.h
config.status: executing libtool commands
config.status: executing depfiles commands

And now, we compile:

$ make
make  all-recursive
make[1]: Entering directory '/home/ajacocks/src/diskfit'
Making all in src
make[2]: Entering directory '/home/ajacocks/src/diskfit/src'
Making all in lib
make[3]: Entering directory '/home/ajacocks/src/diskfit/src/lib'
/bin/sh ../../libtool  --tag=CC   --mode=compile gcc -DHAVE_CONFIG_H -I. -I../..  -DHAVE_INLINE -DGSL_C99_INLINE  -fvisibility=hidden -ffast-math -fstrict-aliasing  -finline-functions -g -O2 -MT libdiskfit_la-libdiskfit.lo -MD -MP -MF .deps/libdiskfit_la-libdiskfit.Tpo -c -o libdiskfit_la-libdiskfit.lo `test -f 'libdiskfit.c' || echo './'`libdiskfit.c
libtool: compile:  gcc -DHAVE_CONFIG_H -I. -I../.. -DHAVE_INLINE -DGSL_C99_INLINE -fvisibility=hidden -ffast-math -fstrict-aliasing -finline-functions -g -O2 -MT libdiskfit_la-libdiskfit.lo -MD -MP -MF .deps/libdiskfit_la-libdiskfit.Tpo -c libdiskfit.c  -fPIC -DPIC -o .libs/libdiskfit_la-libdiskfit.o
libtool: compile:  gcc -DHAVE_CONFIG_H -I. -I../.. -DHAVE_INLINE -DGSL_C99_INLINE -fvisibility=hidden -ffast-math -fstrict-aliasing -finline-functions -g -O2 -MT libdiskfit_la-libdiskfit.lo -MD -MP -MF .deps/libdiskfit_la-libdiskfit.Tpo -c libdiskfit.c -o libdiskfit_la-libdiskfit.o >/dev/null 2>&1
(trimmed for brevity)
/usr/bin/sed -e 's|@bindir[@]|/opt/diskfit/bin|g' < diskfit.bc.in > diskfit
make[2]: Leaving directory '/home/ajacocks/src/diskfit'
make[1]: Leaving directory '/home/ajacocks/src/diskfit'

And now, we install:

$ sudo make install
Making install in src
make[1]: Entering directory '/home/ajacocks/src/diskfit/src'
Making install in lib
make[2]: Entering directory '/home/ajacocks/src/diskfit/src/lib'
make[3]: Entering directory '/home/ajacocks/src/diskfit/src/lib'
 /usr/bin/mkdir -p '/opt/diskfit/lib'
 /bin/sh ../../libtool   --mode=install /usr/bin/install -c   libdiskfit.la '/opt/diskfit/lib'
(trimmed for brevity)

 /usr/bin/install -c -m 644 README.md '/opt/diskfit/share/doc/diskfit'
make[2]: Leaving directory '/home/ajacocks/src/diskfit'
make[1]: Leaving directory '/home/ajacocks/src/diskfit'

The results:

$ tree /opt/diskfit
/opt/diskfit
├── bin
│   └── diskfit
├── etc
│   └── diskfitrc
├── include
│   └── diskfit
│       └── diskfit.h
├── lib
│   ├── libdiskfit.a
│   ├── libdiskfit.la
│   ├── libdiskfit.so -> libdiskfit.so.1.0.2
│   ├── libdiskfit.so.1 -> libdiskfit.so.1.0.2
│   └── libdiskfit.so.1.0.2
└── share
    ├── doc
    │   └── diskfit
    │       └── README.md
    └── man
        └── man1
            └── diskfit.1

11 directories, 10 files

I hope that this helps someone, or in all honestly, me. This is something that I've had to research every time that I have done it, since I don't do it often, and my memory isn't all that good.

Monday, September 30, 2024

PowerDNS and MariaDB (MySQL) Replication - What do you do when things get out of whack?

I use PowerDNS to manage my fairly extensive home lab environment's DNS. Because DNS is needed for not only the lab, but also so that my wife and daughter can watch TV or to use any other 'net-based resource, keeping things up and running is critical. Because of that, I have 3 seperate DNS servers, linked via MariaDB replication. Generally, this has been very reliable, and the configuration is much simpler than my previous setup, which used PowerDNS replication.

However, a couple of times, now, things have gotten out of sync. And, because it happens very rarely, I always forget how to get things resynced. Lucky me, though, user David Espart posted an excellent step-by-step process for getting things going again, here.

I'll repeat the steps below, in case Stack Overflow ever goes away or loses the content:

This is the full step-by-step procedure to resync a master-slave replication from scratch:

At the master:

RESET MASTER;
FLUSH TABLES WITH READ LOCK;
SHOW MASTER STATUS;


And copy the values of the result of the last command somewhere.

Without closing the connection to the client (because it would release the read lock) issue the command to get a dump of the master:

mysqldump -u root -p --all-databases > /a/path/mysqldump.sql

Now you can release the lock, even if the dump hasn't ended yet. To do it, perform the following command in the MySQL client:

UNLOCK TABLES;


Now copy the dump file to the slave using scp or your preferred tool.

At the slave:

Open a connection to mysql and type:

STOP SLAVE;

Load master's data dump with this console command:

mysql -uroot -p < mysqldump.sql

Sync slave and master logs:

RESET SLAVE;
CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=98;


Where the values of the above fields are the ones you copied before.

Finally, type:

START SLAVE;

To check that everything is working again, after typing:

SHOW SLAVE STATUS;

you should see:

Slave_IO_Running: Yes
Slave_SQL_Running: Yes


That's it!

Hope this helps someone, some day, or just jogs my own failing memory.

Monday, September 2, 2024

The Plague of Education Debt

Today, I responded to a Facebook comment that complained about the difficulty of repaying graduate school education debt. The original poster and spouse apparently left graduate school in 2001-2002 with a total of ~$70,000 in debt. After paying $500/mo for the past 23 years, they are still $60,000 in debt.

Many would argue that $500/mo is too small of a monthly payment amount, but given average salaries after graduate school completion, it could well be all the couple could afford.

The original poster asked why education debt should not be cancelled. The responses were predictable, and varied from the sympathetic to those who thought that the borrowers had erred, and must therefore suffer the consequences, to those who commented that cancelling education debt directly adds to taxpayer burden.

Why assume that taxpayers have to foot the bill?

This is clearly (given the high interest rate) a private student loan. Loan cancellation would only affect the private lender, and since the principal amount has been paid, and then almost paid again, they have been adequately compensated. No need for a treasury outlay, just cancel the loan.

I’m sure that someone will call me “communist” or “socialist,” for this view, but I know exactly how the private lending system works. Loans are offered to young people with little experience in borrowing who are hoping that their education will lead to higher lifetime earnings, as we always tell young people. The terms are often unclear, and the interest rates border on the extortionate.

These loans have variable interest rates, and all kinds of fees and penalties, and can leave borrowers in terrible situations, with virtually no way out, since private loans are almost always not discharged in bankruptcy. (see https://scholarlycommons.law.emory.edu/ebdj/vol32/iss1/11/)

I also did not borrow much money to go to college, but the fact that I did not led me to have to both work full time and to attend college at the same time, which helped to cause me to drop out in my 5th year, as a senior. There were indeed other factors involved, including my own errors, but money was a huge concern.

If I had been offered some large private loan, I surely would have taken it, as an inexperienced young person. I am only protected from that by my age, since the cost of education in compared to average salary was much lower in the 1990s.

The rising cost of education, and the flood of high-interest private education loans are a direct threat to the future of this country. We already live in a world where it is not a good financial prospect to go to medical school and become a general practitioner. (see https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4226775/)

Given that the need for education is greater each year, since there are fewer and fewer jobs that do not require a college degree, and the complexity of the world continues to rise, why do we, as a nation, feel that it is in our best interest to place the burden of education financing on the student, who is least well equipped both to understand it, and to pay it off?

Wednesday, July 31, 2024

Creating PDFs of Vintage Manuals - Harder Than it Appears?

I have accumulated quite a bit of documentation, over the years. It's always been on my "things to do someday" list to scan what I have, and upload it to Internet Archive. However, recently, I was really annoyed because I needed a manual that the only source for was a very expensive copy, on eBay. After grinding my teeth about it for a while, I purchased the manual, and waited for it to arrive.

Once it showed up at my house, it was at least well packaged, and in good condition. After this, I tried to take apart a less valuable manual, that was also spiral bound, for scanning. I am lucky enough to have a decent Brother duplexing MFP that also has an ADF for duplex scanning. That's where my luck ran out, though. I quickly discovered that my printer both was unable to reliably feed books, with their shiny covers, and with their varying weight of paper. I found a workaround, where I would separately scan the covers on the flatbed, and then run the pages through the ADF. My next issue was that the MFP refused to duplex scan 8x10" documents. To get past that issue, I scanned all the pages single-sided, and used the really cool pdftk utility to interleave the pages together into a single PDF.

Now that the scanning part was done, I took a look at the result. Unfortunately, it wasn't fantastic. Quite a few of the pages, which were on a fairly lightweight bond paper, had significant bleed-through of the text on their backs to the front, due to the brightness of the scanner's lamp. After talking to a friend who does a lot more book scanning than I do, it was suggested to me to use GraphicConverter. That turned out to be a great idea, and I was able to change pages like this into to ones like this.

This worked, but it was a heck of a lot of work. I quickly realized that I was going to be far too lazy to do this more than a few times. I spoke to my friend again, and they suggested that I get a dedicated ADF scanner that was well-thought-of by folks who do a lot of scanning. After a bit of research, I settled on a Kodak i2600 Document Scanner that was well-used, but fully functional, and available for a reasonable price on eBay. That scanner turned out to be exactly as described, and worked immediately, after I figured out how to properly unfold the input and output trays. With that scanner, I was able to much more easily generate output that looks like this.

To document this process, here is exactly what I did:

  1. remove the spiral binding from the book
  2. split the book into ~50 page sections
  3. run each section through the Kodak i2600, generating individual page TIFF images
  4. convert each TIFF image into a PDF
    1. for doc in *; do echo $doc; convert $doc $doc.pdf; done
  5. reduce the size of the very large (>100MB) individual pages to something more reasonable
    1. for file in *; do if [ ! -d $file ]; then echo $file; pdf2ps $file ../pdf/$( echo $file | sed 's/ps$/pdf/' ); fi; done
    2. for file in *; do if [ ! -d $file ]; then echo $file; ps2pdf $file ../pdf/$( echo $file | sed 's/ps$/pdf/' ); fi; done
  6. last, I used pdftk to join the individual pages into a single book
    1. pdftk `ls -1` cat output merged.pdf
  7. that's it!

I'll be looking to improve the process, but this has worked well enough, so far.

 

Wednesday, February 21, 2024

Bluetooth Low-Energy Devices, Windows and Linux

 I've attempted, many times, to get bluetooth devices paired to multiple OSes simultaneously, on the same machine, and never succeeded. Usually, the OSes involved are Windows and Linux, as I primarily use Linux, but use Windows for certain special apps that require it.

The user 'Mygod' posted a really, really useful GitHub gist, a number of years ago, that solves the problem, rather neatly:
https://gist.github.com/Mygod/f390aabf53cf1406fc71166a47236ebf

The only dependencies are:

  1. having your Windows partition mounted somewhere on your Linux system
  2. installing the 'chntpw' package installed

Once you have both of those, download the script from the gist, which I have included here, for convenience:

#!/usr/bin/python3
"""
Copyright 2021 Mygod

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


What is this: Export your Windows Bluetooth LE keys into Linux!

Thanks to:
* http://console.systems/2014/09/how-to-pair-low-energy-le-bluetooth.html
* https://gist.github.com/corecoding/eac76d3da20c7e427a1848b8aed8e334/revisions#diff-6eeb0d27c24cc10680e8574f75648585

Usage:

$ ./export-ble-infos.py 
$ sudo bash -c 'cp -r ./bluetooth /var/lib && service bluetooth force-reload'
$ rm -r bluetooth
"""

import os
import shutil
import subprocess
import sys
import tempfile

from configparser import ConfigParser
from optparse import OptionParser

default_template = """
[General]
Name=Designer Mouse
Appearance=0x03c2
AddressType=static
SupportedTechnologies=LE;
Trusted=true
Blocked=false
Services=00001800-0000-1000-8000-00805f9b34fb;00001801-0000-1000-8000-00805f9b34fb;0000180a-0000-1000-8000-00805f9b34fb;0000180f-0000-1000-8000-00805f9b34fb;00001812-0000-1000-8000-00805f9b34fb;

[IdentityResolvingKey]
Key=

[LocalSignatureKey]
Key=
Counter=0
Authenticated=false

[LongTermKey]
Key=
Authenticated=0
EncSize=16
EDiv=
Rand=

[DeviceID]
Source=2
Vendor=1118
Product=2053
Version=272

[ConnectionParameters]
MinInterval=6
MaxInterval=6
Latency=60
Timeout=300
"""


def main():
    parser = OptionParser()
    parser.add_option("-v", "--verbose", action='store_true', dest='verbose')
    parser.add_option("-s", "--system", dest="system", metavar="FILE",
                      default="/mnt/Windows/System32/config/SYSTEM",
                      help="SYSTEM file in Windows. Usually at /Windows/System32/config/system.")
    parser.add_option("-k", "--key", dest="key", metavar="KEY",
                      default=r"ControlSet001\Services\BTHPORT\Parameters\Keys",
                      help="Registry key for BT. [default: %default]")
    parser.add_option("-o", "--output", dest="output", metavar="DIR", default="bluetooth",
                      help="Output directory. [default: %default]")
    parser.add_option("-t", "--template", dest="template", metavar="FILE", help="Template file.")
    parser.add_option("-a", "--attributes", dest='attributes', help="Additional attributes file to be copied.")
    options, args = parser.parse_args()

    if options.template:
        with open(options.template) as file:
            template = file.read()
    else:
        template = default_template

    out = tempfile.mktemp(".reg")
    reged = subprocess.Popen(["reged", "-x", options.system, '\\', options.key, out], stdout=sys.stderr)
    reged.wait()
    if reged.returncode:
        return reged.returncode
    dump = ConfigParser()
    with open(out) as file:
        reged_out = file.read()
        if options.verbose:
            print(reged_out)
        dump.read_string(reged_out.split('\n', 1)[1])
    os.unlink(out)

    for section in dump:
        path = section[len(options.key) + 2:].split('\\')
        assert not path[0]
        if len(path) == 3:
            path[1] = ':'.join([path[1][i:i + 2] for i in range(0, len(path[1]), 2)]).upper()
            path[2] = ':'.join([path[2][i:i + 2] for i in range(0, len(path[2]), 2)]).upper()
            print("Dumping {}/{}...".format(path[1], path[2]))
            config = ConfigParser()
            config.optionxform = str

            # See if device has been paired in Linux before
            existing_template = '/var/lib/bluetooth/{}/{}/info'.format(path[1], path[2])
            if (os.path.exists(existing_template)):
                with open(existing_template) as file:
                    config.read_string(file.read())
            else:
                config.read_string(template)

            def read_reg(key, expected_type):
                def read_reg_actual(key, expected_type):
                    actual_type, content = dump[section]['"{}"'.format(key)].split(':', 1)
                    if expected_type == 'hex16':
                        assert actual_type == 'hex'
                        content = content.split(',')
                        assert len(content) == 16
                        return ''.join(content).upper()
                    if expected_type == 'qword':
                        assert actual_type == 'hex(b)'
                        content = content.split(',')
                        assert len(content) == 8
                        return str(int(''.join(content[::-1]), 16))
                    if expected_type == 'dword':
                        assert actual_type == expected_type
                        return str(int(content, 16))
                    assert False
                result = read_reg_actual(key, expected_type)
                if options.verbose:
                    print("{} of type {}: {}".format(key, expected_type, result))
                return result
            config['LongTermKey']['Key'] = read_reg('LTK', 'hex16')
            # KeyLength ignored for now
            config['LongTermKey']['Rand'] = read_reg('ERand', 'qword')
            config['LongTermKey']['EDiv'] = read_reg('EDIV', 'dword')
            if '"IRK"' in dump[section]:
                config['IdentityResolvingKey']['Key'] = read_reg('IRK', 'hex16')
            if '"CSRK"' in dump[section]:
                config['LocalSignatureKey']['Key'] = read_reg('CSRK', 'hex16')
            output_dir = os.path.join(options.output, path[1], path[2])
            os.makedirs(output_dir, exist_ok=True)
            with open(os.path.join(output_dir, 'info'), 'w') as file:
                config.write(file, False)
            if options.attributes:
                shutil.copyfile(options.attributes, os.path.join(output_dir, 'attributes'))


if __name__ == "__main__":
    sys.exit(main())

Once you have the script copied to your system, do the following, noting that your keys will be different than mine:

$ chmod 0755 export-ble-infos.py
$ ./export-ble-infos.py -s /PATH TO YOUR WINDOWS PARTITION/Windows/System32/config/SYSTEM
reged version 0.1 140201, (c) Petter N Hagen
Exporting to file '/tmp/tmppe0q46xu.reg'...
Exporting key 'Keys' with 1 subkeys and 0 values...
Exporting key '50284a36a81b' with 1 subkeys and 1 values...
Exporting key 'ffffcfd7dd52' with 0 subkeys and 10 values...
Dumping 50:28:4A:36:A8:1B/FF:FF:CF:D7:DD:52...
$ sudo cp -r bluetooth /var/lib/
$ sudo systemctl force-reload bluetooth
Now, your device should be linked on Linux as well as Windows, when you boot each OS.

Thanks MyGod!

- Alex