Monday, 02 April

Auto adapting signal to day-night scheme

So I wanted to automatically change the day/night theme of my brand new Signal desktop app. The way that it is manually done is through the File -> Preferences... menu. Now that's not convinient for programatic person like me. Let's investigate!

.config folder

My first stab is looking into the mostly standard ~/.config folder in Linux for Signal if one actually exists. It turns out it does. Let's see. It even has a ~/.config/Signal/config.json file. Unfortunately, that file doesn't contain the settings for the theme, just window positions.

grep -ring in the folder for any of android or theme just returns the logfile that Signal is writing to.

...
./logs/log.log.1:{..."theme-setting changed to android-dark",...}
...

No luck.

Another stab is at /opt/Signal directory. Again - no luck. And the app is a binary ELF executable

$ file signal-desktop 
signal-desktop: ELF 64-bit LSB executable,...

strace-ing

My next attempt is to attach to attach to the Signal process via strace. Find the process by ps aux|grep signal, attach by strace -p <pid> and try to change the . Boy, this does a lot of stuff! Let's filter by writing:

strace -e write -p 12345

Still, too much but I saw something hapenning amid the writing to 40 and 71 file descriptors (fd).

#...
write(40, "!", 1)                       = 1
write(71, "\0", 1)                      = 1
write(40, "!", 1)                       = 1
write(1, "{\"name\":\"log\",\"hostname\":\"pi2-ho"..., 147) = 147
write(12, "\1\0\0\0\0\0\0\0", 8)        = 8
write(71, "\0", 1)                      = 1
write(40, "!", 1)                       = 1
#...

All right, the 1 fd is the log that is being written that I already saw. How about that 12 fd? Let's explore within the proc fs:

$ cd /proc/1234/fd
$ ls -la
# ...
lrwx------ 1 pi2 pi2 64 Mar 29 23:23 12 -> anon_inode:[eventfd]
# ...

Oh boy, some wierd anonymous inode. So we are definately not dealing with a good old filesystem file but rather some on the fly created fd. Let's explore a bit more...

Signal is an Electron app

Taking a look at the github repo of the project. Searching for dark and theme reveals css, sass files. Okay

Looking at the menus there is View -> Toggle Developer Tools and BAM - a good old Chrome developer tools console pops up! Inspecting the elements, it's pretty clear what is happening:

<body class="android">
<!-- changes to -->
<body class="android-dark">

Brilliant, the change of theme just changes the class on the body element. Let's see if we have jquery or I have to go to the dark ages of javascript's findElementByTagName:

> $
< function ( selector, context ) {
        // The jQuery object is actually just the init constructor 'enhanced' ...

Good, we have. Now I "just" have to find a way to connect to chrome console via a good old terminal in some way...

Chrome remote debugging

Searching the Internets, I find a post in Chromium blog from way back in 2011 that I can run Chrome with --remote-debugging-port flag which enables some sort of a remote console.

Would the electron app do the same?

signal-desktop --remote-debugging-port=9222

Indeed it does. This console however is not just a simple tcp connection to the javascript console. Running echo "alert('echo'); | netcat localhost 9222 does nothing. More reading to go...

Turns out, Chrome DevTools has its own protocol that among other things exposes websockets. Let's see what clients exist for that

Quite a lot.

Any pythonic ones? pychrome seems simple enough.

Let's be modern, do python3:

pip3 install pychrome

And I will use my favourite repl, ipython and follow the Getting started tutorial:

import pychrome

browser = pychrome.Browser(url="http://127.0.0.1:9222")

Awesome, I have a connection. The tutorial goes into opening a tab. I don't want that, I assume I should alread have a tab. Exploring browser object (via my favourite ipython repl):

In [3]: dir(browser)
Out[3]: [... _private_stuff
'activate_tab',
 'close_tab',
 'dev_url',
 'list_tab',
 'new_tab',
 'version']

In [4]: browser.list_tab()
Out[4]: [<Tab [38473d8e-ad8d-4325-8cb6-81aaec36c124]>]

In [5]: tab = browser.list_tab()[0]

In [6]: dir(tab)
Out[6]: [... _private_stuff
 'call_method',
 'debug',
 'del_all_listeners',
 'event_handlers',
 'event_queue',
 'get_listener',
 'id',
 'method_results',
 'set_listener',
 'start',
 'status',
 'status_initial',
 'status_started',
 'status_stopped',
 'stop',
 'type',
 'wait']

The README goes into navigating pages and the examples folder is also not what I am looking for. But they do tell me call_method is probably what I want, just need to find the proper string and parameters in the protocl. I want to execute a javascript call.

Back to the Chrome DevTools Protocol Viewer. I look through a few of the Domains listed and finally arrive at Runtime.evaluate method.

In [7]: tab.start()
In [8]: tab.call_method('Runtime.evaluate', expression='alert("hi");')
# I GET AN ALERT IN THE BROWSER! Dismiss it.
Out[8]: {'result': {'type': 'undefined'}}

Finally! I have access to my javascript console in the browser! Let's write the jquery change of theme:

tab.call_method('Runtime.evaluate', expression='$("body").removeClass("android").addClass("android-dark");')

That worked!

Wrapping it up

Let's write a propper python script now:

#!/usr/bin/env python3

import sys
import pychrome

sys_argv = sys.argv

if len(sys_argv) < 2:
    print("first argument needs to be day|night")  
    exit(1)

command = sys_argv[1]

if command == "night":
    expression = js_dark_to_light = """
$("body").removeClass("android").addClass("android-dark");
"""
elif command == "day":
    expression = js_light_to_dark = """
$("body").removeClass("android-dark").addClass("android");
""" 
else:
    print("first argument needs to be day|night")
    exit(1)

browser = pychrome.Browser(url="http://127.0.0.1:9222")
tab = browser.list_tab()[0]
tab.start()

tab.call_method('Runtime.evaluate', expression=expression)

tab.stop()

Running ./change_signal.py day changes to white theme and ./change_signal.py night goes to dark theme.

I also want to change the signal-desktop app to always start with the remote-debug flag enabled, so I do that:

rm /usr/local/bin/signal-desktop
cat > /usr/local/bin/signal-desktop << EOF
#!/bin/bash
/opt/Signal/signal-desktop --remote-debugging-port=9222
EOF
chmod +x /usr/local/bin/signal-desktop

And that's it. Code can be found in my sun repo which also deals with other day-night related dark-light themes including a script to detect whether the sun has set or not:

$ ./calc_sun.py 
usage: calc --loc=location_name [--date-format] 
       calc --lat=latitude --lon=longitude [--tz|--date-format]
returns 
       current_time sunrise sunset day|night

$ ./calc_sun.py --lat=51.21 --lon=4.40 --tz=Europe/Brussels
23:57 07:23 20:10 night

So now I can chain that:

state=`./current.sh | cut -f4 -d' '`
./change_signal.py $state

Voala! That was fun 🙂😊