aboutsummaryrefslogtreecommitdiffstats
path: root/l/src/main.lisp
AgeCommit message (Collapse)Author
2021-02-27Move everything from `l/` into the project rootTeddy Wing
This is the final project. Now that we got rid of the web extension and native host code, we can move the Lisp code to the root.
2021-02-27main: Add documentationTeddy Wing
2021-02-27main: Remove unused globalsTeddy Wing
I used these for debugging, but they aren't relevant any more.
2021-02-27main: Move command line option definitions to `option.lisp`Teddy Wing
Makes more sense to group the option code together.
2021-02-27ws-on-message: Move predicate checking tab reload exception to functionTeddy Wing
Give this check a name.
2021-02-27Hide `websocket-send` debug output behind `--debug` flagTeddy Wing
Make the config containing command line options a global variable. This makes it easy to find out if the `--debug` flag is on in the `websocket-send` function. Not sure about the global variable, but seems fine for now.
2021-02-21When `chrome.tabs.reload()` errors, double decrement the wait groupTeddy Wing
We added an extra increment to the wait group in the `reload-tab` function because when the `chrome.tabs.reload()` message succeeds, we receive two response messages for a single one sent. When an error comes back from the message, however, only one message is received instead of two. We must thus decrement the wait group an extra time to account for the lack of a second response message. Here's what the message sequence looks like: #<WAIT-GROUP :counter 6> Sending: {"id":2,"sessionId":"C67B0BF214B28E1233277D01B897438E","method":"Runtime.evaluate","params":{"expression":"chrome.tabs.reload()"}} Response: (OBJ (id . 2) (result OBJ (result OBJ (type . object) (subtype . error) (className . TypeError) (description . TypeError: Cannot read property 'reload' of undefined at <anonymous>:1:13) (objectId . 1547982045888266898.197.11)) (exceptionDetails OBJ (exceptionId . 15) (text . Uncaught) (lineNumber . 0) (columnNumber . 12) (scriptId . 1569) (exception OBJ (type . object) (subtype . error) (className . TypeError) (description . TypeError: Cannot read property 'reload' of undefined at <anonymous>:1:13) (objectId . 1547982045888266898.197.12)))) (sessionId . C67B0BF214B28E1233277D01B897438E)) #<WAIT-GROUP :counter 7> Sending: {"id":2,"sessionId":"C67B0BF214B28E1233277D01B897438E","method":"Runtime.evaluate","params":{"expression":"chrome.tabs.reload()"}} Response: (OBJ (id . 2) (result OBJ (result OBJ (type . object) (subtype . error) (className . TypeError) (description . TypeError: Cannot read property 'reload' of undefined at <anonymous>:1:13) (objectId . 1547982045888266898.197.13)) (exceptionDetails OBJ (exceptionId . 16) (text . Uncaught) (lineNumber . 0) (columnNumber . 12) (scriptId . 1569) (exception OBJ (type . object) (subtype . error) (className . TypeError) (description . TypeError: Cannot read property 'reload' of undefined at <anonymous>:1:13) (objectId . 1547982045888266898.197.14)))) (sessionId . C67B0BF214B28E1233277D01B897438E)) #<WAIT-GROUP :counter 8> Sending: {"id":2,"sessionId":"C67B0BF214B28E1233277D01B897438E","method":"Runtime.evaluate","params":{"expression":"chrome.tabs.reload()"}} Response: (OBJ (id . 2) (result OBJ (result OBJ (type . undefined))) (sessionId . C67B0BF214B28E1233277D01B897438E)) #<WAIT-GROUP :counter 9>
2021-02-21Remove hard-coded call ID commentsTeddy Wing
We no longer have hard-coded call IDs, so these comments are outdated. The new call ID system seems to also contribute to keeping tab reloading working consistently, so I'm keeping it instead of going back to the hard-coded IDs.
2021-02-21reload-tab: Fix inconsistent tab reloadingTeddy Wing
Finally managed to figure out why tab reloading was inconsistent. With my added logs, I learned that _two_ responses come back from the `chrome.tabs.reload()` message we send instead of one. This means that the wait group gets decremented prematurely, and the program could exit before we've transmitted the message(s). To fix this, increment the wait group an extra time when sending the tab reload message to account for the second message response. Here are the logs: One tab reloaded, one not: #<WAIT-GROUP :counter 1> Sending: {"id":2,"method":"Target.attachToTarget","params":{"targetId":"E1DA65BA7761486BBA9B341E5403744F","flatten":true}} Sending: {"id":3,"method":"Target.attachToTarget","params":{"targetId":"84AFD21582910FC66AD0FB24E410BBC1","flatten":true}} Response: (OBJ (method . Target.attachedToTarget) (params OBJ (sessionId . C2BBFC26103DAB67ECC199C87CB38B86) (targetInfo OBJ (targetId . E1DA65BA7761486BBA9B341E5403744F) (type . background_page) (title . TITLE) (url . chrome-extension://EXTENSION_ID/_generated_background_page.html) (attached . T) (canAccessOpener) (browserContextId . 29837CF1E6AC13B4FDA7DD2883E6320E)) (waitingForDebugger))) #<WAIT-GROUP :counter 2> Sending: {"id":1,"sessionId":"C2BBFC26103DAB67ECC199C87CB38B86","method":"Runtime.evaluate","params":{"expression":"chrome.runtime.reload()"}} Response: (OBJ (id . 2) (result OBJ (sessionId . C2BBFC26103DAB67ECC199C87CB38B86))) #<WAIT-GROUP :counter 2> Sending: {"id":2,"sessionId":"C2BBFC26103DAB67ECC199C87CB38B86","method":"Runtime.evaluate","params":{"expression":"chrome.tabs.reload()"}} Response: (OBJ (method . Target.attachedToTarget) (params OBJ (sessionId . 9ED44EDF0DA25CE52F21AA90EBAEF7D7) (targetInfo OBJ (targetId . 84AFD21582910FC66AD0FB24E410BBC1) (type . background_page) (title . TITLE) (url . chrome-extension://EXTENSION_ID/_generated_background_page.html) (attached . T) (canAccessOpener) (browserContextId . E3DEC22E4F9847AAA674ACF4781D51A9)) (waitingForDebugger))) #<WAIT-GROUP :counter 2> Sending: {"id":3,"sessionId":"9ED44EDF0DA25CE52F21AA90EBAEF7D7","method":"Runtime.evaluate","params":{"expression":"chrome.runtime.reload()"}} Response: (OBJ (id . 3) (result OBJ (sessionId . 9ED44EDF0DA25CE52F21AA90EBAEF7D7))) #<WAIT-GROUP :counter 2> Sending: {"id":4,"sessionId":"9ED44EDF0DA25CE52F21AA90EBAEF7D7","method":"Runtime.evaluate","params":{"expression":"chrome.tabs.reload()"}} Response: (OBJ (id . 1) (result OBJ (result OBJ (type . undefined))) (sessionId . C2BBFC26103DAB67ECC199C87CB38B86)) #<WAIT-GROUP :counter 2> Response: (OBJ (id . 3) (result OBJ (result OBJ (type . undefined))) (sessionId . 9ED44EDF0DA25CE52F21AA90EBAEF7D7)) #<WAIT-GROUP :counter 1> Both tabs reloaded: #<WAIT-GROUP :counter 1> Sending: {"id":2,"method":"Target.attachToTarget","params":{"targetId":"84AFD21582910FC66AD0FB24E410BBC1","flatten":true}} Sending: {"id":3,"method":"Target.attachToTarget","params":{"targetId":"E1DA65BA7761486BBA9B341E5403744F","flatten":true}} Response: (OBJ (method . Target.attachedToTarget) (params OBJ (sessionId . A1BD02811FD42011A5325723F2D31819) (targetInfo OBJ (targetId . 84AFD21582910FC66AD0FB24E410BBC1) (type . background_page) (title . TITLE) (url . chrome-extension://EXTENSION_ID/_generated_background_page.html) (attached . T) (canAccessOpener) (browserContextId . E3DEC22E4F9847AAA674ACF4781D51A9)) (waitingForDebugger))) #<WAIT-GROUP :counter 2> Sending: {"id":1,"sessionId":"A1BD02811FD42011A5325723F2D31819","method":"Runtime.evaluate","params":{"expression":"chrome.runtime.reload()"}} Response: (OBJ (id . 2) (result OBJ (sessionId . A1BD02811FD42011A5325723F2D31819))) #<WAIT-GROUP :counter 2> Sending: {"id":2,"sessionId":"A1BD02811FD42011A5325723F2D31819","method":"Runtime.evaluate","params":{"expression":"chrome.tabs.reload()"}} Response: (OBJ (method . Target.attachedToTarget) (params OBJ (sessionId . 4DC48E49A8133A13BED664D9C466F892) (targetInfo OBJ (targetId . E1DA65BA7761486BBA9B341E5403744F) (type . background_page) (title . TITLE) (url . chrome-extension://EXTENSION_ID/_generated_background_page.html) (attached . T) (canAccessOpener) (browserContextId . 29837CF1E6AC13B4FDA7DD2883E6320E)) (waitingForDebugger))) #<WAIT-GROUP :counter 2> Sending: {"id":3,"sessionId":"4DC48E49A8133A13BED664D9C466F892","method":"Runtime.evaluate","params":{"expression":"chrome.runtime.reload()"}} Response: (OBJ (id . 3) (result OBJ (sessionId . 4DC48E49A8133A13BED664D9C466F892))) #<WAIT-GROUP :counter 2> Sending: {"id":4,"sessionId":"4DC48E49A8133A13BED664D9C466F892","method":"Runtime.evaluate","params":{"expression":"chrome.tabs.reload()"}} Response: (OBJ (id . 1) (result OBJ (result OBJ (type . undefined))) (sessionId . A1BD02811FD42011A5325723F2D31819)) #<WAIT-GROUP :counter 2> Response: (OBJ (id . 2) (result OBJ (result OBJ (type . undefined))) (sessionId . A1BD02811FD42011A5325723F2D31819)) #<WAIT-GROUP :counter 1> Response: (OBJ (id . 3) (result OBJ (result OBJ (type . undefined))) (sessionId . 4DC48E49A8133A13BED664D9C466F892)) #<WAIT-GROUP :counter 0> Response: (OBJ (id . 4) (result OBJ (result OBJ (type . undefined))) (sessionId . 4DC48E49A8133A13BED664D9C466F892)) #<WAIT-GROUP :counter -1>
2021-02-21websocket-send: Add debug output for WebSocket message sendTeddy Wing
Realised I'm only printing the received messages. It would help to see what messages we send and when too. Still having the inconsistent tab reload problem, so it looks like my 16c8122254725805be666cf065c590d54d66dfad change didn't fix it.
2021-02-21Add `--debug` option to print debug outputTeddy Wing
Let's keep a way to print WebSocket messages for debugging purposes in the release build rather than remove the messages completely. Since I've been struggling with the messages so much it seems like this could be a useful thing to have.
2021-02-21main: Replace hard-coded call IDs with a new counterTeddy Wing
Looks like this was the answer to the problem of the active tab not reloading consistently. Previously, if I had two instances of an extension loaded in two separate Chrome profiles, the following could happen: 1. Neither profile's active tab reloaded 2. Only one profile's active tab reloaded 3. Both profiles' active tabs reloaded With this change, both profiles' active tabs reload correctly and consistently.
2021-02-21ws-on-message: Adjust debug messagesTeddy Wing
Move the response print calls to the top of the WebSocket message receiver to make debug output chronology easier to follow.
2021-02-14main.lisp: Rename `reload-extensions` to `attach-extensions`Teddy Wing
That function was originally intended to be doing reloading, but that didn't turn out to be the case. In order to do the reloading, we first need to attach to the extensions individually. Rename the function to better correspond to what it does.
2021-02-14Timeout after five secondsTeddy Wing
Set a five second timeout to ensure we don't block endlessly if we send a WebSocket message and don't receive a response in a reasonable amount of time. Chose five seconds arbitrarily, but that seems like more than enough time to do our work. If it takes longer, something else is probably wrong.
2021-02-14main.lisp: Remove handled TODOs and clean up commentsTeddy Wing
2021-02-13Replace exit codes with constants from the `sysexits` systemTeddy Wing
2021-02-13Add a generic "print error and exit" functionTeddy Wing
I wanted to centralise the formatting of error printing.
2021-02-09Always reload current tab after reloading an extensionTeddy Wing
A bit crude, but it covers us when extensions are in multiple Chrome profiles. This ensures the desired tab is reloaded because all current tabs in all profiles that have requested extensions are reloaded. Not very intelligent, but a simple approach.
2021-02-09Use proper extension target count for active tab reload conditionTeddy Wing
Didn't reload the active tab in my test. But the previous way doesn't work when multiple copies of the same extension appear in the list of targets. Think we might want to always reload the current tab after reloading an extension because the extension targets could be in different Chrome profiles. Thus, you couldn't be sure which tab would be reloaded.
2021-02-09Keep trying to reload tab until it succeedsTeddy Wing
If we reload the active tab and it fails, we'll get an error response back. Keep trying to reload the page until we no longer get the error.
2021-02-08Sort of found working reload current tab implementationTeddy Wing
Tried using manually-incremented `*reloaded-count*` but that didn't quite work as I didn't have the right conditions to start the reload. Then tried setting up the condition such that we reload when the response comes back from the extension reload message. We can tell this when we get a `result` response that includes a `sessionId` field. To execute the reload a single time, store the most recent session ID, and compare against that. Using conditions on both the reloaded count (which needs to be changed to handle multiple copies of the same extension) and the last session ID in the message contents, we have enough to set up the reload in the tab. Added the `sleep` call back in because otherwise I got this error: reloading NOW Response: (OBJ (id . 2) (result OBJ (sessionId . 106D182E44C641B22EC65E9F6458B245))) #<WAIT-GROUP :counter 2> Response: (OBJ (id . 1) (result OBJ (result OBJ (type . object) (subtype . error) (className . TypeError) (description . TypeError: Cannot read property 'reload' of undefined at <anonymous>:1:16) (objectId . 8548451452974044825.19.1)) (exceptionDetails OBJ (exceptionId . 2) (text . Uncaught) (lineNumber . 0) (columnNumber . 15) (scriptId . 147) (exception OBJ (type . object) (subtype . error) (className . TypeError) (description . TypeError: Cannot read property 'reload' of undefined at <anonymous>:1:16) (objectId . 8548451452974044825.19.2)))) (sessionId . 106D182E44C641B22EC65E9F6458B245)) #<WAIT-GROUP :counter 1> Response: (OBJ (id . 2) (result OBJ (result OBJ (type . object) (subtype . error) (className . TypeError) (description . TypeError: Cannot read property 'reload' of undefined at <anonymous>:1:13) (objectId . 8548451452974044825.19.3)) (exceptionDetails OBJ (exceptionId . 3) (text . Uncaught) (lineNumber . 0) (columnNumber . 12) (scriptId . 156) (exception OBJ (type . object) (subtype . error) (className . TypeError) (description . TypeError: Cannot read property 'reload' of undefined at <anonymous>:1:13) (objectId . 8548451452974044825.19.4)))) (sessionId . 106D182E44C641B22EC65E9F6458B245)) That tells us that the tab reload message was sent to the extension's background page before it had a chance to fully reload. We thus need to wait until the extension is fully reloaded before being able to send the tab reload message. Don't like the sleep call here. Would be nice to have a more robust solution that didn't wait an arbitrary amount of time. Maybe we can keep sending the tab reload message until we get a response that's not an error.
2021-02-08Try reloading active tab after reloading all extensionsTeddy Wing
Trying to set up the reload on the active tab after all extensions are reloaded. It's only working half the time and I can't figure out what I'm doing wrong. Something wrong with the wait group I think. Committing what I have and might try other ideas.
2021-02-06reload-extensions: Add TODO to renameTeddy Wing
2021-02-06main: Add TODO reminderTeddy Wing
2021-02-05main: Exit on uncaught errors and print the messageTeddy Wing
Previously, we'd drop into the debugger on uncaught errors. Instead, we should just exit with an error message.
2021-02-04Make DevTools Protocol call ID auto-incrementingTeddy Wing
Remove the hard-coded call IDs and replace them with a class that keeps track of the current call ID and allows for easy incrementing to get the next ID. This should allow us to give multiple extension IDs on the command line and send messages with properly incrementing call IDs. Didn't touch the `runtime-evaluate-msg` message call ID because that one is local to the target it's attached to, so we can keep it at ID "1".
2021-02-03main: Replace hard-coded extension ID with list from command line argsTeddy Wing
Reload the extension IDs given on the command line now that we have them from the `config` object. Replace the hard-coded extension ID that I had been using for testing.
2021-02-03main: Get WebSocket URL from command line for `*client*`Teddy Wing
Keep the `*client*` global variable, but use the client in `config`, constructed from the WebSocket URL passed from the command line. Not a huge fan of the global variable, but it's way easier to keep it like this rather than passing the client down to the `attach-to-target` and `reload-extension` functions.
2021-02-03config: Initialise a new websocket-driver client on new configTeddy Wing
Add a new `make-config` function to construct a `config` object. This creates a new websocket-driver client and stores it in the `ws-client` slot in the `config`. Before this, I thought about using a writer method on the `socket-url` slot that creates a new client. This didn't work in `make-instance` though. Perhaps there's a way to have the `:initarg` use the writer, but I'm not sure.
2021-02-03Move DevTools functions to `devtools-protocol.lisp`Teddy Wing
The `main.lisp` file was getting crowded. Move DevTools Protocol-related functions into a new file.
2021-02-03main: Move option parsing code to new function `parse-options`Teddy Wing
Make a new function `parse-options` that parses the command line options and returns a `config` object. We'll use that object instead of `options` in `main`. Cleans up the `main` function a bit. Currently, we just print the `config` object to ensure we're storing the proper values. Followed Practical Common Lisp's example to implement `print-object` so we can see the contents of its slots: http://www.gigamonkeys.com/book/practical-a-spam-filter.html#the-heart-of-a-spam-filter Still need to implement error checking for a missing `--socket-url` option.
2021-02-02main: Handle command line argument parsing errorsTeddy Wing
Add an error handler that just prints the error messages from 'unix-opts' to standard error and exits with EX_USAGE. Inspired by: - http://lispcookbook.github.io/cl-cookbook/scripting.html#handling-malformed-or-missing-arguments - https://github.com/libre-man/unix-opts/blob/0e61f34b2ecf62288437810d4abb31e572048b04/example/example.lisp
2021-02-02main: Show help output with -h/--helpTeddy Wing
Add `:arg-parser` to the `:socket-url` definition because 'unix-opts' requires it in order to display the "SOCKET_URL" `:meta-var` in the help output.
2021-02-02main: Start command line option parsingTeddy Wing
Include the 'unix-opts' library described in http://lispcookbook.github.io/cl-cookbook/scripting.html#parsing-command-line-arguments for command line option parsing. Define the options I need. We want a `--socket-url` option, and a list of extension IDs as free arguments. Implement the `-V` version command line argument. Thanks to JJJ (https://stackoverflow.com/users/1337941/jjj) on Stack Overflow for describing how to get the version number of an ASDF system: https://stackoverflow.com/questions/11084339/getting-the-version-of-an-asdf-system/11088022#11088022 Add a new `options.lisp` file where we'll add the option parsing restart error handling functions required by 'unix-opts'.
2021-02-01main.lisp: Update testing WebSocket URLTeddy Wing
2021-02-01main: Ensure WebSocket connection is always closedTeddy Wing
Create a new `with-websocket-connection` decorator that starts and closes a WebSocket connection to the given `client` around the body forms. Thanks to Practical Common Lisp's "The Special Operators" chapter (http://www.gigamonkeys.com/book/the-special-operators.html#unwinding-the-stack) for introducing me to `unwind-protect`. Couldn't figure out how to get the new macro to auto-indent properly with Vlime, so ended up manually indenting it.
2021-02-01websocket-send: Use local `client` variable instead of globalTeddy Wing
Must have copy-pasted that s-expression from elsewhere in the file.
2021-01-31main: Replace `sleep` call with a `wait-group`Teddy Wing
The `sleep` call allowed me to test the behaviour of the program, since without it, it would exit before the WebSocket messages had a chance to be sent and received. But we shouldn't be waiting a fixed number of seconds for the program to execute. Instead, we should only keep the program alive as long as there are messages to be sent and received. This adds a Go-style wait group using my wait-group library that increments the wait group when we send a WebSocket message, and decrements it when we receive a WebSocket response. That allows us to keep the program alive only for the amount of time necessary for the messages to be exchanged.
2021-01-31Convert `filter` from a function to a macroTeddy Wing
Doesn't make a big difference, just for fun. I like the idea of a compile-time version of this since it's essentially just renaming `remove-if-not`. Move it to a new file so we can include it before it's used in `main.lisp`.
2021-01-30main.lisp: Use `find-if` in `requested-extension-p`Teddy Wing
I've been reading Practical Common Lisp's "Collections" chapter (http://www.gigamonkeys.com/book/collections.html) and it seemed like `find-if` would be nicer here than what I wrote before.
2021-01-30main.lisp: Change `filter` to use `remove-if-not`Teddy Wing
Just learned about `remove-if-not`. Really cleans up this function. Not even really necessary to keep `filter`, but I guess I'll hold on to it for now.
2021-01-30extension-targets: Use `string=` instead of `equal`Teddy Wing
Just learned that `string=` exists, and that seems to describe the intent better than `equal`.
2021-01-30main.lisp: Update temporary WebSocket URLTeddy Wing
2021-01-24main.lisp: Reload extensionsTeddy Wing
Send DevTools Protocol messages to reload extensions. Not easy to do things sequentially since the responses have to be handled in `ws-on-message`. Once we filter the list of extensions wanted to reload, attach to their DevTools targets, then send them JavaScript evaluation messages that tells the extensions to reload.
2021-01-24main.lisp: Rename `get-targets-msg` to `target-get-targets-msg`Teddy Wing
Prefix the function name with the name of the DevTools domain to distinguish Target message functions from Runtime functions.
2021-01-24filter: Fix `let` binding syntaxTeddy Wing
2021-01-24main.lisp: Filter DevTools targets to chosen extensionsTeddy Wing
Filter a list of extension background page targets. The extension IDs should come from the command line, but I've hard-coded the list here. Increased the `sleep` time to allow time for the messages to be sent & received before the program exits. Will need to figure out a proper way to do this later.
2021-01-24main.lisp: Parse `Target.getTargets` responseTeddy Wing
Use `jsown` to parse the response from the `Target.getTargets` message. Get a list of `targetInfos` from the response. Extracting keys from the JSON result with `jsown:val` raises an `error` exception when the key is not present. Turn the exception into `nil` with the `json-obj-get` function.
2021-01-23Send `Target.getTargets` messageTeddy Wing
Add a `sleep` just in case we need that to have time to print the message. Use the `jsown` project for JSON encoding and decoding.