211 Commits

Author SHA1 Message Date
Pierre Tachoire
58ca323918 window: implement a debug() function
In order to ease debug log in js world
2024-11-28 16:31:30 +01:00
Francis Bouvier
8f7a8c0ee1 Merge pull request #309 from lightpanda-io/update_deps
update zig-js-runtime
2024-11-26 12:59:19 +01:00
Francis Bouvier
7dbb55da06 update zig-js-runtime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-26 12:58:45 +01:00
Pierre Tachoire
4b90b70534 Merge pull request #308 from lightpanda-io/licensing
add licensing file to be more explicit w/ licenses
2024-11-26 12:52:16 +01:00
Pierre Tachoire
d6f1843ef3 add licensing file to be more explicit w/ licenses 2024-11-26 12:30:50 +01:00
Pierre Tachoire
5cf176927a Merge pull request #303 from lcoffe-botify/fix-makefile-download-zig
fix Makefile download-zig: url changed
2024-11-26 11:03:31 +01:00
Pierre Tachoire
eae21a034e Merge pull request #306 from lightpanda-io/cla
contrib: add CLA signature process
2024-11-26 10:31:17 +01:00
Pierre Tachoire
06855cdfa3 Merge pull request #307 from lightpanda-io/clean-ci
ci: remove useless token
2024-11-26 09:52:05 +01:00
Pierre Tachoire
45c7af9769 ci: remove useless token
The repos are public now, we don't need the token anymore to fetch.
2024-11-26 09:37:40 +01:00
Pierre Tachoire
f9ddd5c368 contrib: add CLA signature process 2024-11-26 09:35:14 +01:00
Pierre Tachoire
6bf4dc887e Merge pull request #305 from lightpanda-io/docker-ca-cert
docker: keep ca certs
2024-11-25 11:51:10 +01:00
Pierre Tachoire
4cc31bb41f docker: keep ca certs 2024-11-25 09:03:52 +01:00
Lucien Coffe
88e32505a3 fix Makefile download-zig: url changed 2024-11-22 18:50:11 +01:00
Francis Bouvier
5b5d28f7c1 Merge pull request #302 from lightpanda-io/zig-async-io
Use zig-async-io for xhr requests
2024-11-21 16:52:36 +01:00
Francis Bouvier
1a2fd9a584 Update dependencies
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-21 16:46:09 +01:00
Francis Bouvier
de286dd78e async: use zig-async-io
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-21 16:42:48 +01:00
Pierre Tachoire
70752027f1 async: remove @This from SigleThreaded 2024-11-19 15:55:26 +01:00
Pierre Tachoire
395eb3e8ad async: add missing tests execution 2024-11-19 15:54:04 +01:00
Pierre Tachoire
e1137274fb async: remove dead code 2024-11-19 15:51:36 +01:00
Francis Bouvier
bc716ef0ad Merge pull request #299 from lightpanda-io/cli_refacto
cli: code refacto
2024-11-19 15:31:19 +01:00
Pierre Tachoire
d2d2e851b0 async: fix assync call pop error 2024-11-18 17:39:38 +01:00
Pierre Tachoire
9149b60136 async: remove dead code 2024-11-18 17:39:38 +01:00
Pierre Tachoire
18ab0c8199 cdp: replace tick by run_for_ns 2024-11-18 17:39:38 +01:00
Pierre Tachoire
7fed1f3015 async: remove pseudo-async http client 2024-11-18 17:39:37 +01:00
Pierre Tachoire
6809bb5393 async: adapt async cli 2024-11-18 17:39:37 +01:00
Pierre Tachoire
fadf3f609a http: add full async client 2024-11-18 17:39:37 +01:00
Pierre Tachoire
8d2d089803 Merge pull request #301 from lightpanda-io/better-log
Better log
2024-11-18 14:39:40 +01:00
Pierre Tachoire
f6ad95c647 improve event's log result 2024-11-18 13:13:52 +01:00
Pierre Tachoire
0788e22dd5 typo fix 2024-11-18 13:13:23 +01:00
Francis Bouvier
de260693dc Merge pull request #296 from lightpanda-io/memory_leak
cdp: fix memory leak in msg parsing of the JSON
2024-11-15 02:17:01 +01:00
Francis Bouvier
f60fcbec04 Merge pull request #286 from lightpanda-io/cdp-refacto-input
cdp: refacto message JSON read
2024-11-15 01:03:53 +01:00
Francis Bouvier
4f99407462 Merge pull request #288 from lightpanda-io/cdp-create-target
cdp: browserContextId is optional in Target.createTarget
2024-11-15 00:53:22 +01:00
Pierre Tachoire
38813a20a7 Merge pull request #298 from lightpanda-io/docker-warning
docker: use absolute path with WORKDIR
2024-11-14 08:56:34 +01:00
Pierre Tachoire
2cd1e927f7 docker: use absolute path with WORKDIR
remove following the warning
```
 1 warning found (use docker --debug to expand):
 - WorkdirRelativePath: Relative workdir "browser" can have unexpected results if the base image changes (line 48)
 ```
2024-11-14 08:46:01 +01:00
Pierre Tachoire
5094942560 cdp: add msg tests into zig build test 2024-11-12 12:56:30 +01:00
Pierre Tachoire
82c37fc71b cdp: refacto message JSON read 2024-11-12 12:56:29 +01:00
Pierre Tachoire
8ba911c8dd cdp: return provided browser context id if any 2024-11-12 10:56:06 +01:00
Francis Bouvier
ac77453139 cli: code refacto
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-10 13:53:07 +01:00
Francis Bouvier
8a25545cac memory: use a GPA in Debug mode and a page allocator in Release
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-09 13:34:15 +01:00
Francis Bouvier
ed3a464843 cdp: fix memory leak in msg parsing of the JSON
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-09 03:25:42 +01:00
Francis Bouvier
1854074f64 Merge pull request #293 from lightpanda-io/cdp-contextid
cdp: use a u32 for context id
2024-11-07 15:49:40 +01:00
Francis Bouvier
ec5de2fce0 Merge pull request #287 from lightpanda-io/cdp-attach-to-target
cdp: add Target.attachToTarget noop
2024-11-07 15:49:15 +01:00
Francis Bouvier
3af34d11ca Merge pull request #291 from lightpanda-io/multi_build
Multi build
2024-11-06 18:17:24 +01:00
Francis Bouvier
eed7b7186d Merge pull request #284 from lightpanda-io/server-sync-deinit
server: ensure Send is always deinit in callback
2024-11-06 18:17:10 +01:00
Francis Bouvier
d5e7ebdc63 Merge pull request #295 from lightpanda-io/fix_cdp_full_async
Fix cdp full async
2024-11-06 18:14:43 +01:00
Francis Bouvier
3ecfa6aca8 Dockerfile: add install-libiconv
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-06 18:10:08 +01:00
Francis Bouvier
625c1741c6 Update zig-js-runtime (tigerbeetle)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-06 18:07:43 +01:00
Francis Bouvier
f6f5ec5eb3 server: add cancel current recv before accepting new connection
Only on Linux. On MacOS cancel is not supported for now and
we do not have any problem with the current recv operation
on a closed socket.

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-06 18:07:43 +01:00
Francis Bouvier
c74feb9c3a server: add log on I/O errors
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-05 17:16:39 +01:00
Pierre Tachoire
0d76f80223 cdp: use a u32 for context id 2024-11-04 10:08:36 +01:00
Pierre Tachoire
1e64513c16 Merge pull request #292 from lightpanda-io/tcp_nodelay
server: set TCP.NODELAY on linux to avoid latency issues
2024-11-04 10:04:25 +01:00
Francis Bouvier
64779acf32 Merge pull request #278 from lightpanda-io/cdp_full_async
Cdp full async
2024-11-01 18:14:21 +01:00
Francis Bouvier
c3a3ac19f4 server: set TCP.NODELAY on linux to avoid latency issues
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-11-01 17:54:49 +01:00
Francis Bouvier
b9bae3f66d build: update gitignore
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-30 13:41:48 +01:00
Francis Bouvier
2a2486cbe0 build: fix clean-libiconv
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-30 13:41:34 +01:00
Pierre Tachoire
0813d99b44 Merge pull request #290 from lightpanda-io/dockerfile
dockerfile: adjust binary name after merge
2024-10-30 10:42:15 +01:00
Pierre Tachoire
491e89d102 dockerfile: adjust binary name after merge 2024-10-30 10:40:59 +01:00
Francis Bouvier
f01558251c Merge pull request #277 from lightpanda-io/merge_bin
Merge get and server binaires
2024-10-29 22:27:09 +01:00
Francis Bouvier
8665d0420b Merge pull request #282 from lightpanda-io/docker-build
add a Dockerfile to build the project
2024-10-29 22:20:49 +01:00
Francis Bouvier
cf0636ca63 Update src/main.zig usage
Co-authored-by: Pierre Tachoire <pierre@lightpanda.io>
2024-10-29 22:19:44 +01:00
Francis Bouvier
46d0aa6f9e Remove all references to the name 'browsercore'
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-29 22:16:56 +01:00
Francis Bouvier
b9e2be2052 build: support multi os/arch conf for netsurf
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-29 20:06:29 +01:00
Pierre Tachoire
b3054d68bf cdp: browserContextId is optional in Target.createTarget
https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-createTarget
2024-10-29 10:37:23 +01:00
Pierre Tachoire
60adf0a9c3 cdp: add Target.attachToTarget noop 2024-10-29 10:34:36 +01:00
Francis Bouvier
be5d7022cc build: support multi os/arch conf for libiconv
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-28 21:08:46 +01:00
Francis Bouvier
d1951b286c build: support multi os/arch conf for mimalloc
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-28 16:06:41 +01:00
Pierre Tachoire
dcdef2f640 server: ensure Send is always deinit in callback 2024-10-25 09:51:37 +02:00
Pierre Tachoire
7afe74310f add a Dockerfile to build the project 2024-10-23 15:22:12 +02:00
Pierre Tachoire
826f82610e Merge pull request #280 from lightpanda-io/cdpdump-close-dir
cdp: close dir in dumpFile
2024-10-23 10:15:31 +02:00
Pierre Tachoire
5d7796b95d cdp: close dir in dumpFile
and avoid error.ProcessFdQuotaExceeded error
2024-10-23 10:02:34 +02:00
Pierre Tachoire
b3ac313cc7 Merge pull request #279 from lightpanda-io/ci-ubuntu
ci: use ubuntu 22.04 for nightly build
2024-10-22 15:43:39 +02:00
Pierre Tachoire
b281ba7754 ci: use zig-v8 0.1.9 2024-10-22 15:03:01 +02:00
Pierre Tachoire
10994b202b ci: use ubuntu latest for all expect nightly build 2024-10-22 14:27:47 +02:00
Pierre Tachoire
2aeac1bdeb ci: force ubuntu 22.04 for nightly build
To ensure a better compatibility.
2024-10-22 14:27:10 +02:00
Francis Bouvier
8508c21080 cdp: remove send sync
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-21 18:29:10 +02:00
Francis Bouvier
20dd140c31 cdp: send I/O next read before executing current cmd
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-21 18:21:43 +02:00
Francis Bouvier
486c19079a Merge get and server binaires
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-18 16:06:23 +02:00
Pierre Tachoire
f30501ca3c Merge pull request #276 from lightpanda-io/compare-position
node: implement node.compareDocumentPosition
2024-10-17 15:06:47 +02:00
Pierre Tachoire
e67e6e267b Merge pull request #275 from lightpanda-io/fake-css-properties
html: implement empty style property
2024-10-17 15:06:40 +02:00
Pierre Tachoire
8dc757ddf3 node: implement getRootNode 2024-10-17 14:44:34 +02:00
Pierre Tachoire
b64f7d013d node: implement node.compareDocumentPosition 2024-10-17 14:44:33 +02:00
Francis Bouvier
62ec936f1e Merge pull request #215 from lightpanda-io/cdp_basic
CDP basic
2024-10-17 10:52:09 +02:00
Francis Bouvier
8d83dfad45 ci: force ubuntu version (24.04)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-17 10:24:00 +02:00
Pierre Tachoire
e450072f45 ci: add zig v8 version into the cache key 2024-10-16 21:00:44 +02:00
Francis Bouvier
7f08d08a78 Update zig-v8 again
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 17:54:41 +02:00
Francis Bouvier
b0634cd871 Adapt wpt and shell to zig-js-runtime changes
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 15:21:03 +02:00
Francis Bouvier
462485bfcb Update zig-v8 and zig-js-runtime deps
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 14:56:04 +02:00
Francis Bouvier
2311765289 Remove some dead code
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 14:53:50 +02:00
Francis Bouvier
7bc7da5499 browser: back on createPage returning a Page (pointer)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-16 14:53:11 +02:00
Pierre Tachoire
b712a4771e html: implement empty style property 2024-10-16 10:22:23 +02:00
Francis Bouvier
8e05f09fc8 server, cdp: improve logging
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-15 22:57:56 +02:00
Francis Bouvier
84c49fbe34 cdp: ensure there is an ID on each request
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-15 17:28:18 +02:00
Francis Bouvier
7750956c7b msg: Add a more complex test case with 2 multipart messages combined
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-15 16:07:46 +02:00
Francis Bouvier
ea9af210f9 Remove heap allocation for Session
And adapt to similar changes on zig-js-runtime for Env

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-15 15:52:48 +02:00
Francis Bouvier
efca71510a browser: put back VM is an arg for browser init
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-12 10:41:59 +02:00
Francis Bouvier
cbf6348055 server: panic if sendInspector without an inspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-12 10:38:53 +02:00
Francis Bouvier
ec680593b0 msg: set a hard limit max size
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-11 18:13:20 +02:00
Francis Bouvier
fd6c25daaa msg: improve comments on reallocation
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-11 18:05:04 +02:00
Francis Bouvier
4b495f213f cdp: add comment on hard coded ID for page.createIsolatedWorld
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 15:21:09 +02:00
Francis Bouvier
7ad03fb548 cdp: fix a comment on page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 15:18:55 +02:00
Francis Bouvier
17c641845e msg: return error if input does not have "size:"
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 15:13:06 +02:00
Francis Bouvier
e53b9d984b browser: add comment for auxData param in page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 15:10:30 +02:00
Francis Bouvier
28593d93ff browser: panic if callInspector without Inspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:47:14 +02:00
Francis Bouvier
fa4920bd94 browser: rename setInspector -> initInspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:45:28 +02:00
Francis Bouvier
eaf5c6f86f cdp: ensure method action is present
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:42:20 +02:00
Francis Bouvier
0d89b98bad cdp: ensure token is a string when needed in parser
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:35:56 +02:00
Francis Bouvier
bf56345e48 msg: comments typos
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:19:35 +02:00
Francis Bouvier
2bc58bebce server: rename public -> jsruntime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:11:43 +02:00
Francis Bouvier
c564702eac server: formatting
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:10:54 +02:00
Francis Bouvier
9400dd799e Add cli options for server (host, port, timeout)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 12:06:39 +02:00
Francis Bouvier
ff0bbc3f96 server: simplify Send I/O
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 01:21:24 +02:00
Francis Bouvier
15414f5ee4 server: remove unused sendLater
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 01:00:12 +02:00
Francis Bouvier
f9b097794f Simplify browser session.setInspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 00:58:13 +02:00
Francis Bouvier
a2f65eb540 server: simplify onInspector methods
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 00:56:15 +02:00
Francis Bouvier
cea38a10e9 server: rename buf in read_buf
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 00:56:11 +02:00
Francis Bouvier
c8a91d4cf6 server: merge Cmd and Accept in Ctx
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-09 00:55:29 +02:00
Francis Bouvier
b0ff325125 server: move to TCP conn
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-08 23:44:47 +02:00
Francis Bouvier
c35c09db60 server: timeout mechanism
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-08 23:40:50 +02:00
Francis Bouvier
49adb61146 server: handle close and re-open connection
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-08 16:22:24 +02:00
Francis Bouvier
76a9034668 server: newSession on disposeBrowserContext
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-07 21:14:55 +02:00
Francis Bouvier
4c225e515d server: let the caller of sendSync free the string
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-07 16:04:29 +02:00
Francis Bouvier
9c913b2e6c Move loop outside Browser
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-07 15:57:16 +02:00
Francis Bouvier
5ab1d2a8a5 Add License in new cdp files
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 18:02:21 +02:00
Francis Bouvier
2f3a581859 Add TODOs and comments
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 17:48:54 +02:00
Francis Bouvier
8bdd2a14e8 Add Target.disposeBrowserContext
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 17:13:47 +02:00
Francis Bouvier
1675f69582 Add Target.closeTarget
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 17:13:29 +02:00
Francis Bouvier
94d2d28806 Redirect Runtime domain to JS engine Inspector
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-10-01 17:12:08 +02:00
Pierre Tachoire
82a5e50056 Merge pull request #274 from lightpanda-io/upgrade-libdom
upgrade libdom
2024-09-25 11:53:28 +02:00
Pierre Tachoire
46062e185a upgrade libdom 2024-09-25 11:47:34 +02:00
Pierre Tachoire
6929141210 Merge pull request #273 from lightpanda-io/nodelist-iterator
nodelist: remove debug log
2024-09-25 09:51:00 +02:00
Pierre Tachoire
cce36c5fbd nodelist: remove debug log 2024-09-25 09:50:31 +02:00
Pierre Tachoire
2518287326 Merge pull request #272 from lightpanda-io/nodelist-iterator
nodelist: implement iterators
2024-09-25 09:42:59 +02:00
Pierre Tachoire
aefab86501 nodelist: implement iterators 2024-09-25 09:37:14 +02:00
Pierre Tachoire
30679d18ee Merge pull request #271 from lightpanda-io/currentscript
implement DOM document.currentscript
2024-09-24 10:14:21 +02:00
Pierre Tachoire
95c0ff6f39 dom: implement currentScript 2024-09-24 10:01:13 +02:00
Pierre Tachoire
4d6f59ecb8 upgrade libdom 2024-09-24 10:01:12 +02:00
Pierre Tachoire
4b5668f4fd Merge pull request #270 from lightpanda-io/nodelist-foreach
DOM: implement nodelist.foreach
2024-09-20 18:37:22 +02:00
Pierre Tachoire
44a5fa011a dom: implement nodelist.foreach 2024-09-20 18:32:23 +02:00
Francis Bouvier
14a3a662fd Fix response of runtime.Evaluate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-07-09 16:10:25 +02:00
Francis Bouvier
41409031fd Adapt to refacto in js_exec from zig-js-runtime
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-07-08 22:51:41 +02:00
Francis Bouvier
ea410c8ced Fix changes in Zig 0.12 std lib
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-20 00:32:06 +02:00
Francis Bouvier
aca64eedca Uniformize calling name conventions
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-19 15:56:44 +02:00
Francis Bouvier
0f8b47b598 Move MsgBuffer in it's own file for unit test purpose
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-19 15:48:20 +02:00
Francis Bouvier
5eae15889d Add some optional fields in Runtime.evaluate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-19 15:23:09 +02:00
Francis Bouvier
9319e4a7f1 Handle Runtime.callFunctionOn
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-17 16:35:22 +02:00
Francis Bouvier
4d756b5bfc Add a dumpFile utility function
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-17 16:34:47 +02:00
Francis Bouvier
409969621d Add Runtime.addBinding
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-12 17:56:54 +02:00
Francis Bouvier
7abb7277c9 Fix call to Runtime.executionContextCreated in Page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-12 17:56:07 +02:00
Francis Bouvier
9120b9c1de Add emulation.setTouchEmulationEnabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:19:08 +02:00
Francis Bouvier
08c11ac41f Add performance.enable
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:16:15 +02:00
Francis Bouvier
cecc03e1ed Add fetch.disable
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:12:31 +02:00
Francis Bouvier
7d67d131c2 Add network.setCacheDisabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:08:49 +02:00
Francis Bouvier
1929eed8ac Add contextID in state
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:03:12 +02:00
Francis Bouvier
ad8c9fac2b Add target.setDiscoverTargets
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:02:37 +02:00
Francis Bouvier
fa82160265 Add target.getBrowserContexts
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 16:02:02 +02:00
Francis Bouvier
dc1456f4e8 Handle CDP messages with different order
The 'method' still needs to be the first or the second key
(in this case after the 'id').

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-06-07 15:59:57 +02:00
Francis Bouvier
3ad19dffa1 Handle CDP msg with order <id, method> and <method, id>
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-30 17:43:01 +02:00
Francis Bouvier
bfb9db235e Basic Runtime.evaluate run
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-30 16:21:18 +02:00
Francis Bouvier
c57e50c5b9 Handle Runtime.evaluate (no-op)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-27 16:02:14 +02:00
Francis Bouvier
bafdca3ffa MsgBuffer to handle both combined and multipart read
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-05-22 16:24:39 +02:00
Francis Bouvier
ba12945e5b Move read input from Cmd callback to allow unit tests
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-24 11:17:55 +02:00
Francis Bouvier
96906df64b Implement own protocol to handle msg size
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-23 12:48:35 +02:00
Francis Bouvier
3396c70b67 Send Runtime.executionContextCreated events in Page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-23 10:44:33 +02:00
Francis Bouvier
28d5c682cd Use sendEvent in Runtime.executionContextCreated and expose it
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-23 10:44:03 +02:00
Francis Bouvier
7a03562a33 Typo fix Page.LifecycleEvent
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-23 10:43:11 +02:00
Francis Bouvier
4a31dd8aa3 Let Page.navigate do actually navigation
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 17:13:32 +02:00
Francis Bouvier
1b1b7cdfb0 Add page_life_cycle_events in CDP state
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 17:12:37 +02:00
Francis Bouvier
9e13ffb8ff Add sendEvent utility function
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 17:11:31 +02:00
Francis Bouvier
ed38705efd Basic version using Browser
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 11:57:44 +02:00
Francis Bouvier
1a1cd0353c Add dummy Page.navigate
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 11:36:02 +02:00
Francis Bouvier
4f0b071c59 Fix getContent algo
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-19 11:35:47 +02:00
Francis Bouvier
9ce574a1f0 Add Page.createIsolatedWorld
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 21:57:31 +02:00
Francis Bouvier
c54b50eb0c Add Browser.setWindowBounds
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 21:52:06 +02:00
Francis Bouvier
aec7455151 Add Emulation.setDeviceMetricsOverride
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 21:46:43 +02:00
Francis Bouvier
c7ba567d7f Handle non-empty void params in getContent
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 21:45:46 +02:00
Francis Bouvier
fc1b3d5397 Contextual frameTree
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 20:54:30 +02:00
Francis Bouvier
508741c367 Add Browser.getWindowForTarget
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 20:53:18 +02:00
Francis Bouvier
f02de77295 Add getContent
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 20:38:27 +02:00
Francis Bouvier
9974b56607 Add Target.createTarget
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 16:43:19 +02:00
Francis Bouvier
0506a7bb53 Add Browser.createBrowserContext
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 16:14:19 +02:00
Francis Bouvier
06f161c423 Add Target.getTargetInfo
+ do not send attachedToTarget if sessionId

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 13:20:23 +02:00
Francis Bouvier
69f5bb9ed3 Add sessionId in Runime.runIfWaitingForDebugger response
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 13:18:51 +02:00
Francis Bouvier
490eb40028 Add method cdp function
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 13:18:16 +02:00
Francis Bouvier
43a558f5ae Make getParams return nullable
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 12:10:20 +02:00
Francis Bouvier
e4ae2df1a4 Add some optional params in methods
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 11:57:39 +02:00
Francis Bouvier
1620138421 Return sessionId in Emulation.setFocusEmulationEnabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 11:32:11 +02:00
Francis Bouvier
e59fc903f2 Return a result in Page.getFrameTree
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-18 10:20:47 +02:00
Francis Bouvier
4d8cdc6dc8 Handle sessionId in result
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-17 14:18:18 +02:00
Francis Bouvier
21afa1f4b3 Do not emit optional null value in JSON output
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-17 14:04:34 +02:00
Francis Bouvier
05c5d06df5 Change Page.addScriptToEvaluateOnNewDocument
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 17:28:28 +02:00
Francis Bouvier
9e8b765f7a Allow method with sessionId and use it when appropriate (*.enable)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 17:19:50 +02:00
Francis Bouvier
36dbc28bde Add Runtime.runIfWaitingForDebugger
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 16:40:50 +02:00
Francis Bouvier
26eda90f7e Add setFocusEmulationEnabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 16:38:47 +02:00
Francis Bouvier
211fa3d947 Handle several JSON msg in 1 read
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 16:38:15 +02:00
Francis Bouvier
67bbd9957d Add Network domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 01:03:04 +02:00
Francis Bouvier
aff2250504 Add Emulation domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 01:02:44 +02:00
Francis Bouvier
86b1c851c0 Add Page.addScriptToEvaluateOnNewDocument
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:57:25 +02:00
Francis Bouvier
0a03dcb465 Add Page.setLifecycleEventsEnabled
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:54:40 +02:00
Francis Bouvier
e073e3388d Add Runtime domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:50:17 +02:00
Francis Bouvier
626fae0da0 Add Log domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:48:40 +02:00
Francis Bouvier
a708a7f387 Add Page.getFrameTree
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:48:17 +02:00
Francis Bouvier
b1242207a9 Add Page domain
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:41:26 +02:00
Francis Bouvier
980571073d Big refacto
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-16 00:38:06 +02:00
Francis Bouvier
5e1fe656e8 send Target.attachedToTarget after Target.setAutoAttach
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 21:34:00 +02:00
Francis Bouvier
ffbfd36502 Add stringify function in cdp
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 21:33:32 +02:00
Francis Bouvier
e908cb0ec4 Use send as normal behavior in cmdCallback
+ add nanoseconds param in sendLater

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 21:32:14 +02:00
Francis Bouvier
95a64b7696 Handle concurrent calls to sendLater
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 17:57:33 +02:00
Francis Bouvier
cfd6fc9532 Working sendLater (I/O timeout)
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 17:36:22 +02:00
Francis Bouvier
defab0c774 Free msg at the right place
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 15:52:13 +02:00
Francis Bouvier
babac692d5 Remove alloc from CmdContext struct
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 15:47:19 +02:00
Francis Bouvier
c57bb9ef72 WIP: CDP
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-04-15 12:14:33 +02:00
55 changed files with 3933 additions and 2696 deletions

View File

@@ -17,7 +17,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.6'
default: 'v0.1.9'
v8:
description: 'v8 version to install'
required: false
@@ -47,7 +47,7 @@ runs:
cache-name: cache-v8
with:
path: ${{ inputs.cache-dir }}/v8
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}_${{ inputs.zig-v8 }}.a
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
shell: bash

View File

@@ -16,13 +16,12 @@ jobs:
ARCH: x86_64
OS: linux
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
@@ -32,13 +31,13 @@ jobs:
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64
- name: Rename binary
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly
build-macos-aarch64:
@@ -52,7 +51,6 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
@@ -65,11 +63,11 @@ jobs:
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8
- name: Rename binary
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: nightly

32
.github/workflows/cla.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
permissions:
actions: write
contents: read
pull-requests: write
statuses: write
jobs:
CLAAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
uses: contributor-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_GH_PAT }}
with:
path-to-signatures: 'signatures/browser/version1/cla.json'
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
# branch should not be protected
branch: 'main'
allowlist: krichprollsch,francisbouvier
remote-organization-name: lightpanda-io
remote-repository-name: cla

View File

@@ -50,7 +50,6 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive

View File

@@ -48,7 +48,6 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
@@ -69,7 +68,6 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive
@@ -90,7 +88,6 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_CI_PAT }}
# fetch submodules recusively, to get zig-js-runtime submodules also.
submodules: recursive

4
.gitignore vendored
View File

@@ -1,7 +1,5 @@
zig-cache
/.zig-cache/
zig-out
/vendor/netsurf/build/
/vendor/netsurf/lib/
/vendor/netsurf/include/
/vendor/netsurf/out
/vendor/libiconv/

3
.gitmodules vendored
View File

@@ -25,3 +25,6 @@
[submodule "vendor/tls.zig"]
path = vendor/tls.zig
url = git@github.com:ianic/tls.zig.git
[submodule "vendor/zig-async-io"]
path = vendor/zig-async-io
url = git@github.com:lightpanda-io/zig-async-io.git

93
CLA.md Normal file
View File

@@ -0,0 +1,93 @@
# Lightpanda (Selecy SAS) Grant and Contributor License Agreement (“Agreement”)
This agreement is based on the Apache Software Foundation Contributor License
Agreement. (v r190612)
Thank you for your interest in software projects stewarded by Lightpanda
(Selecy SAS) (“Lightpanda”). In order to clarify the intellectual property
license granted with Contributions from any person or entity, Lightpanda must
have a Contributor License Agreement (CLA) on file that has been agreed to by
each Contributor, indicating agreement to the license terms below. This license
is for your protection as a Contributor as well as the protection of Lightpanda
and its users; it does not change your rights to use your own Contributions for
any other purpose. This Agreement allows an individual to contribute to
Lightpanda on that individuals own behalf, or an entity (the “Corporation”) to
submit Contributions to Lightpanda, to authorize Contributions submitted by its
designated employees to Lightpanda, and to grant copyright and patent licenses
thereto.
You accept and agree to the following terms and conditions for Your present and
future Contributions submitted to Lightpanda. Except for the license granted
herein to Lightpanda and recipients of software distributed by Lightpanda, You
reserve all right, title, and interest in and to Your Contributions.
1. Definitions. “You” (or “Your”) shall mean the copyright owner or legal
entity authorized by the copyright owner that is making this Agreement with
Lightpanda. For legal entities, the entity making a Contribution and all
other entities that control, are controlled by, or are under common control
with that entity are considered to be a single Contributor. For the purposes
of this definition, “control” means (i) the power, direct or indirect, to
cause the direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
“Contribution” shall mean any work, as well as any modifications or
additions to an existing work, that is intentionally submitted by You to
Lightpanda for inclusion in, or documentation of, any of the products owned
or managed by Lightpanda (the “Work”). For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication
sent to Lightpanda or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems (such
as GitHub), and issue tracking systems that are managed by, or on behalf of,
Lightpanda for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise designated
in writing by You as “Not a Contribution.”
2. Grant of Copyright License. Subject to the terms and conditions of this
Agreement, You hereby grant to Lightpanda and to recipients of software
distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable copyright license to reproduce, prepare derivative
works of, publicly display, publicly perform, sublicense, and distribute
Your Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of this
Agreement, You hereby grant to Lightpanda and to recipients of software
distributed by Lightpanda a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable (except as stated in this section) patent license
to make, have made, use, offer to sell, sell, import, and otherwise transfer
the Work, where such license applies only to those patent claims licensable
by You that are necessarily infringed by Your Contribution(s) alone or by
combination of Your Contribution(s) with the Work to which such
Contribution(s) were submitted. If any entity institutes patent litigation
against You or any other entity (including a cross-claim or counterclaim in
a lawsuit) alleging that your Contribution, or the Work to which you have
contributed, constitutes direct or contributory patent infringement, then
any patent licenses granted to that entity under this Agreement for that
Contribution or Work shall terminate as of the date such litigation is
filed.
4. You represent that You are legally entitled to grant the above license. If
You are an individual, and if Your employer(s) has rights to intellectual
property that you create that includes Your Contributions, you represent
that You have received permission to make Contributions on behalf of that
employer, or that Your employer has waived such rights for your
Contributions to Lightpanda. If You are a Corporation, any individual who
makes a contribution from an account associated with You will be considered
authorized to Contribute on Your behalf.
5. You represent that each of Your Contributions is Your original creation (see
section 7 for submissions on behalf of others).
6. You are not expected to provide support for Your Contributions,except to the
extent You desire to provide support. You may provide support for free, for
a fee, or not at all. Unless required by applicable law or agreed to in
writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation, You may
submit it to Lightpanda separately from any Contribution, identifying the
complete details of its source and of any license or other restriction
(including, but not limited to, related patents, trademarks, and license
agreements) of which you are personally aware, and conspicuously marking the
work as “Submitted on behalf of a third-party: [named here]”.

77
Dockerfile Normal file
View File

@@ -0,0 +1,77 @@
FROM ubuntu:22.04
ARG ZIG=0.13.0
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG OS=linux
ARG ARCH=x86_64
ARG V8=11.1.134
ARG ZIG_V8=v0.1.9
RUN apt-get update -yq && \
apt-get install -yq xz-utils \
python3 ca-certificates git \
pkg-config libglib2.0-dev \
gperf libexpat1-dev \
cmake clang \
curl git
# install minisig
RUN curl -L -O https://github.com/jedisct1/minisign/releases/download/0.11/minisign-0.11-linux.tar.gz && \
tar xvzf minisign-0.11-linux.tar.gz
# install zig
RUN curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz && \
curl -O https://ziglang.org/download/${ZIG}/zig-linux-x86_64-${ZIG}.tar.xz.minisig
RUN minisign-linux/x86_64/minisign -Vm zig-linux-x86_64-${ZIG}.tar.xz -P ${ZIG_MINISIG}
# clean minisg
RUN rm -fr minisign-0.11-linux.tar.gz minisign-linux
# install zig
RUN tar xvf zig-linux-x86_64-${ZIG}.tar.xz && \
mv zig-linux-x86_64-${ZIG} /usr/local/lib && \
ln -s /usr/local/lib/zig-linux-x86_64-${ZIG}/zig /usr/local/bin/zig
# clean up zig install
RUN rm -fr zig-linux-x86_64-${ZIG}.tar.xz zig-linux-x86_64-${ZIG}.tar.xz.minisig
# force use of http instead of ssh with github
RUN cat <<EOF > /root/.gitconfig
[url "https://github.com/"]
insteadOf="git@github.com:"
EOF
# clone lightpanda
RUN git clone git@github.com:lightpanda-io/browser.git
WORKDIR /browser
# install deps
RUN git submodule init && \
git submodule update --recursive
RUN cd vendor/zig-js-runtime && \
git submodule init && \
git submodule update --recursive
RUN make install-libiconv && \
make install-netsurf && \
make install-mimalloc
# download and install v8
RUN curl -L -o libc_v8.a https://github.com/lightpanda-io/zig-v8-fork/releases/download/${ZIG_V8}/libc_v8_${V8}_${OS}_${ARCH}.a && \
mkdir -p vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release && \
mv libc_v8.a vendor/zig-js-runtime/vendor/v8/${ARCH}-${OS}/release/libc_v8.a
# build release
RUN make build
FROM ubuntu:22.04
# copy ca certificates
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "3245"]

22
LICENSING.md Normal file
View File

@@ -0,0 +1,22 @@
# Licensing
License names used in this document are as per [SPDX License
List](https://spdx.org/licenses/).
The default license for this project is [AGPL-3.0-only](LICENSE).
## MIT
The following files are licensed under MIT:
```
src/http/Client.zig
```
The following directories and their subdirectories are licensed under their
original upstream licenses:
```
vendor/
tests/wpt/
```

101
Makefile
View File

@@ -4,6 +4,25 @@
ZIG := zig
BC := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# OS and ARCH
kernel = $(shell uname -ms)
ifeq ($(kernel), Darwin arm64)
OS := macos
ARCH := aarch64
else ifeq ($(kernel), Linux aarch64)
OS := linux
ARCH := aarch64
else ifeq ($(kernel), Linux arm64)
OS := linux
ARCH := aarch64
else ifeq ($(kernel), Linux x86_64)
OS := linux
ARCH := x86_64
else
$(error "Unhandled kernel: $(kernel)")
endif
# Infos
# -----
.PHONY: help
@@ -26,30 +45,11 @@ help:
.PHONY: build build-dev run run-release shell test bench download-zig wpt
zig_version = $(shell grep 'recommended_zig_version = "' "vendor/zig-js-runtime/build.zig" | cut -d'"' -f2)
kernel = $(shell uname -ms)
## Download the zig recommended version
download-zig:
ifeq ($(kernel), Darwin x86_64)
$(eval target="macos")
$(eval arch="x86_64")
else ifeq ($(kernel), Darwin arm64)
$(eval target="macos")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux aarch64)
$(eval target="linux")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux arm64)
$(eval target="linux")
$(eval arch="aarch64")
else ifeq ($(kernel), Linux x86_64)
$(eval target="linux")
$(eval arch="x86_64")
else
$(error "Unhandled kernel: $(kernel)")
endif
$(eval url = "https://ziglang.org/builds/zig-$(target)-$(arch)-$(zig_version).tar.xz")
$(eval dest = "/tmp/zig-$(target)-$(arch)-$(zig_version).tar.xz")
$(eval url = "https://ziglang.org/download/$(zig_version)/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
$(eval dest = "/tmp/zig-$(OS)-$(ARCH)-$(zig_version).tar.xz")
@printf "\e[36mDownload zig version $(zig_version)...\e[0m\n"
@curl -o "$(dest)" -L "$(url)" || (printf "\e[33mBuild ERROR\e[0m\n"; exit 1;)
@printf "\e[33mDownloaded $(dest)\e[0m\n"
@@ -69,7 +69,7 @@ build-dev:
## Run the server in debug mode
run: build
@printf "\e[36mRunning...\e[0m\n"
@./zig-out/bin/browsercore || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
@./zig-out/bin/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
## Run a JS shell in debug mode
shell:
@@ -100,10 +100,10 @@ test:
.PHONY: install-dev install
## Install and build dependencies for release
install: install-submodule install-zig-js-runtime install-netsurf install-mimalloc
install: install-submodule install-zig-js-runtime install-libiconv install-netsurf install-mimalloc
## Install and build dependencies for dev
install-dev: install-submodule install-zig-js-runtime-dev install-netsurf-dev install-mimalloc-dev
install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-dev
install-netsurf-dev: _install-netsurf
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
@@ -111,14 +111,16 @@ install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
install-netsurf: _install-netsurf
install-netsurf: OPTCFLAGS := -DNDEBUG
BC_NS := $(BC)vendor/netsurf
ICONV := $(BC)vendor/libiconv
BC_NS := $(BC)vendor/netsurf/out/$(OS)-$(ARCH)
ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
# TODO: add Linux iconv path (I guess it depends on the distro)
# TODO: this way of linking libiconv is not ideal. We should have a more generic way
# and stick to a specif version. Maybe build from source. Anyway not now.
_install-netsurf: install-libiconv
_install-netsurf: clean-netsurf
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
ls $(ICONV) 1> /dev/null || (printf "\e[33mERROR: you need to install libiconv in your system (on MacOS on with Homebrew)\e[0m\n"; exit 1;) && \
ls $(ICONV)/lib/libiconv.a 1> /dev/null || (printf "\e[33mERROR: you need to execute 'make install-libiconv'\e[0m\n"; exit 1;) && \
mkdir -p $(BC_NS) && \
cp -R vendor/netsurf/share $(BC_NS) && \
export PREFIX=$(BC_NS) && \
export OPTLDFLAGS="-L$(ICONV)/lib" && \
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
@@ -156,10 +158,7 @@ _install-netsurf: install-libiconv
clean-netsurf:
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
cd vendor/netsurf && \
rm -R build && \
rm -R lib && \
rm -R include
rm -Rf $(BC_NS)
test-netsurf:
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
@@ -169,14 +168,22 @@ test-netsurf:
cd vendor/netsurf/libdom && \
BUILDDIR=$(BC_NS)/build/libdom make test
install-libiconv:
ifeq ("$(wildcard vendor/libiconv/lib/libiconv.a)","")
download-libiconv:
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
@mkdir -p vendor/libiconv
@cd vendor/libiconv && \
curl https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.17.tar.gz | tar -xvzf -
endif
install-libiconv: download-libiconv clean-libiconv
@cd vendor/libiconv/libiconv-1.17 && \
./configure --prefix=$(BC)vendor/libiconv --enable-static && \
./configure --prefix=$(ICONV) --enable-static && \
make && make install
clean-libiconv:
ifneq ("$(wildcard vendor/libiconv/libiconv-1.17/Makefile)","")
@cd vendor/libiconv/libiconv-1.17 && \
make clean
endif
install-zig-js-runtime-dev:
@@ -188,24 +195,28 @@ install-zig-js-runtime:
make install
.PHONY: _build_mimalloc
_build_mimalloc:
@cd vendor/mimalloc && \
mkdir -p out/include && \
cp include/mimalloc.h out/include/ && \
cd out && \
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) .. && \
make
MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH)
_build_mimalloc: clean-mimalloc
@mkdir -p $(MIMALLOC)/build && \
cd $(MIMALLOC)/build && \
cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \
make && \
mkdir -p $(MIMALLOC)/lib
install-mimalloc-dev: _build_mimalloc
install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
install-mimalloc-dev:
@cd vendor/mimalloc/out && \
mv libmimalloc-debug.a libmimalloc.a
@cd $(MIMALLOC) && \
mv build/libmimalloc-debug.a lib/libmimalloc.a
install-mimalloc: _build_mimalloc
install-mimalloc:
@cd $(MIMALLOC) && \
mv build/libmimalloc.a lib/libmimalloc.a
clean-mimalloc:
@rm -fr vendor/mimalloc/lib/*
@rm -Rf $(MIMALLOC)/build
## Init and update git submodule
install-submodule:

View File

@@ -196,3 +196,10 @@ To add a new test, copy the file you want from the [WPT
repo](https://github.com/web-platform-tests/wpt) into the `tests/wpt` directory.
:warning: Please keep the original directory tree structure of `tests/wpt`.
## Contributing
Lightpanda accepts pull requests through GitHub.
You have to sign our [CLA](CLA.md) during the pull request process otherwise
we're not able to accept your contributions.

105
build.zig
View File

@@ -45,21 +45,17 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const mode = b.standardOptimizeOption(.{});
const options = try jsruntime.buildOptions(b);
const x86 = b.option(bool, "x86", "Use x86 backend") orelse false;
const options = jsruntime.buildOptions(b);
// browser
// -------
// compile and install
const exe = b.addExecutable(.{
.name = "browsercore",
.name = "lightpanda",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, exe, options);
b.installArtifact(exe);
@@ -79,12 +75,10 @@ pub fn build(b: *std.Build) !void {
// compile and install
const shell = b.addExecutable(.{
.name = "browsercore-shell",
.name = "lightpanda-shell",
.root_source_file = b.path("src/main_shell.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, shell, options);
try jsruntime_pkgs.add_shell(shell);
@@ -108,8 +102,6 @@ pub fn build(b: *std.Build) !void {
.test_runner = b.path("src/test_runner.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, tests, options);
@@ -132,12 +124,10 @@ pub fn build(b: *std.Build) !void {
// compile and install
const wpt = b.addExecutable(.{
.name = "browsercore-wpt",
.name = "lightpanda-wpt",
.root_source_file = b.path("src/main_wpt.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, wpt, options);
@@ -149,30 +139,6 @@ pub fn build(b: *std.Build) !void {
// step
const wpt_step = b.step("wpt", "WPT tests");
wpt_step.dependOn(&wpt_cmd.step);
// get
// -----
// compile and install
const get = b.addExecutable(.{
.name = "browsercore-get",
.root_source_file = b.path("src/main_get.zig"),
.target = target,
.optimize = mode,
.use_llvm = !x86,
.use_lld = !x86,
});
try common(b, get, options);
b.installArtifact(get);
// run
const get_cmd = b.addRunArtifact(get);
if (b.args) |args| {
get_cmd.addArgs(args);
}
// step
const get_step = b.step("get", "request URL");
get_step.dependOn(&get_cmd.step);
}
fn common(
@@ -180,38 +146,64 @@ fn common(
step: *std.Build.Step.Compile,
options: jsruntime.Options,
) !void {
const target = step.root_module.resolved_target.?;
const jsruntimemod = try jsruntime_pkgs.module(
b,
options,
step.root_module.optimize.?,
step.root_module.resolved_target.?,
target,
);
step.root_module.addImport("jsruntime", jsruntimemod);
const netsurf = moduleNetSurf(b);
const netsurf = try moduleNetSurf(b, target);
netsurf.addImport("jsruntime", jsruntimemod);
step.root_module.addImport("netsurf", netsurf);
const asyncio = b.addModule("asyncio", .{
.root_source_file = b.path("vendor/zig-async-io/src/lib.zig"),
});
step.root_module.addImport("asyncio", asyncio);
const tlsmod = b.addModule("tls", .{
.root_source_file = b.path("vendor/tls.zig/src/main.zig"),
});
step.root_module.addImport("tls", tlsmod);
}
fn moduleNetSurf(b: *std.Build) *std.Build.Module {
fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
const mod = b.addModule("netsurf", .{
.root_source_file = b.path("src/netsurf/netsurf.zig"),
.target = target,
});
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
// iconv
mod.addObjectFile(b.path("vendor/libiconv/lib/libiconv.a"));
mod.addIncludePath(b.path("vendor/libiconv/include"));
const libiconv_lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
const libiconv_include_path = try std.fmt.allocPrint(
mod.owner.allocator,
"vendor/libiconv/out/{s}-{s}/lib/libiconv.a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(libiconv_lib_path));
mod.addIncludePath(b.path(libiconv_include_path));
// mimalloc
mod.addImport("mimalloc", moduleMimalloc(b));
mod.addImport("mimalloc", (try moduleMimalloc(b, target)));
// netsurf libs
const ns = "vendor/netsurf";
mod.addIncludePath(b.path(ns ++ "/include"));
const ns_include_path = try std.fmt.allocPrint(
mod.owner.allocator,
ns ++ "/out/{s}-{s}/include",
.{ @tagName(os), @tagName(arch) },
);
mod.addIncludePath(b.path(ns_include_path));
const libs: [4][]const u8 = .{
"libdom",
@@ -220,20 +212,35 @@ fn moduleNetSurf(b: *std.Build) *std.Build.Module {
"libwapcaplet",
};
inline for (libs) |lib| {
mod.addObjectFile(b.path(ns ++ "/lib/" ++ lib ++ ".a"));
const ns_lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(ns_lib_path));
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
}
return mod;
}
fn moduleMimalloc(b: *std.Build) *std.Build.Module {
fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
const mod = b.addModule("mimalloc", .{
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
.target = target,
});
mod.addObjectFile(b.path("vendor/mimalloc/out/libmimalloc.a"));
mod.addIncludePath(b.path("vendor/mimalloc/out/include"));
const os = target.result.os.tag;
const arch = target.result.cpu.arch;
const mimalloc = "vendor/mimalloc";
const lib_path = try std.fmt.allocPrint(
mod.owner.allocator,
mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a",
.{ @tagName(os), @tagName(arch) },
);
mod.addObjectFile(b.path(lib_path));
mod.addIncludePath(b.path(mimalloc ++ "/include"));
return mod;
}

View File

@@ -26,6 +26,7 @@ const Events = @import("events/event.zig");
const XHR = @import("xhr/xhr.zig");
const Storage = @import("storage/storage.zig");
const URL = @import("url/url.zig");
const Iterators = @import("iterator/iterator.zig");
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
@@ -38,6 +39,7 @@ pub const Interfaces = generate.Tuple(.{
XHR.Interfaces,
Storage.Interfaces,
URL.Interfaces,
Iterators.Interfaces,
});
pub const UserContext = @import("user_context.zig").UserContext;

File diff suppressed because it is too large Load Diff

View File

@@ -1,133 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
const io = std.io;
const assert = std.debug.assert;
const tcp = @import("tcp.zig");
pub const Stream = struct {
alloc: std.mem.Allocator,
conn: *tcp.Conn,
handle: posix.socket_t,
pub fn close(self: Stream) void {
posix.close(self.handle);
self.alloc.destroy(self.conn);
}
pub const ReadError = posix.ReadError;
pub const WriteError = posix.WriteError;
pub const Reader = io.Reader(Stream, ReadError, read);
pub const Writer = io.Writer(Stream, WriteError, write);
pub fn reader(self: Stream) Reader {
return .{ .context = self };
}
pub fn writer(self: Stream) Writer {
return .{ .context = self };
}
pub fn read(self: Stream, buffer: []u8) ReadError!usize {
return self.conn.receive(self.handle, buffer) catch |err| switch (err) {
else => return error.Unexpected,
};
}
pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize {
return posix.readv(s.handle, iovecs);
}
/// Returns the number of bytes read. If the number read is smaller than
/// `buffer.len`, it means the stream reached the end. Reaching the end of
/// a stream is not an error condition.
pub fn readAll(s: Stream, buffer: []u8) ReadError!usize {
return readAtLeast(s, buffer, buffer.len);
}
/// Returns the number of bytes read, calling the underlying read function
/// the minimal number of times until the buffer has at least `len` bytes
/// filled. If the number read is less than `len` it means the stream
/// reached the end. Reaching the end of the stream is not an error
/// condition.
pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize {
assert(len <= buffer.len);
var index: usize = 0;
while (index < len) {
const amt = try s.read(buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
/// TODO in evented I/O mode, this implementation incorrectly uses the event loop's
/// file system thread instead of non-blocking. It needs to be reworked to properly
/// use non-blocking I/O.
pub fn write(self: Stream, buffer: []const u8) WriteError!usize {
return self.conn.send(self.handle, buffer) catch |err| switch (err) {
error.AccessDenied => error.AccessDenied,
error.WouldBlock => error.WouldBlock,
error.ConnectionResetByPeer => error.ConnectionResetByPeer,
error.MessageTooBig => error.FileTooBig,
error.BrokenPipe => error.BrokenPipe,
else => return error.Unexpected,
};
}
pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
var index: usize = 0;
while (index < bytes.len) {
index += try self.write(bytes[index..]);
}
}
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writev`.
pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize {
if (iovecs.len == 0) return 0;
const first_buffer = iovecs[0].base[0..iovecs[0].len];
return try self.write(first_buffer);
}
/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
/// order to handle partial writes from the underlying OS layer.
/// See https://github.com/ziglang/zig/issues/7699
/// See equivalent function: `std.fs.File.writevAll`.
pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void {
if (iovecs.len == 0) return;
var i: usize = 0;
while (true) {
var amt = try self.writev(iovecs[i..]);
while (amt >= iovecs[i].len) {
amt -= iovecs[i].len;
i += 1;
if (i >= iovecs.len) return;
}
iovecs[i].base += amt;
iovecs[i].len -= amt;
}
}
};

View File

@@ -1,112 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const net = std.net;
const Stream = @import("stream.zig").Stream;
const Loop = @import("jsruntime").Loop;
const NetworkImpl = Loop.Network(Conn.Command);
// Conn is a TCP connection using jsruntime Loop async I/O.
// connect, send and receive are blocking, but use async I/O in the background.
// Client doesn't own the socket used for the connection, the caller is
// responsible for closing it.
pub const Conn = struct {
const Command = struct {
impl: NetworkImpl,
done: bool = false,
err: ?anyerror = null,
ln: usize = 0,
fn ok(self: *Command, err: ?anyerror, ln: usize) void {
self.err = err;
self.ln = ln;
self.done = true;
}
fn wait(self: *Command) !usize {
while (!self.done) try self.impl.tick();
if (self.err) |err| return err;
return self.ln;
}
pub fn onConnect(self: *Command, err: ?anyerror) void {
self.ok(err, 0);
}
pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void {
self.ok(err, ln);
}
};
loop: *Loop,
pub fn connect(self: *Conn, socket: std.posix.socket_t, address: std.net.Address) !void {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.connect(&cmd, socket, address);
_ = try cmd.wait();
}
pub fn send(self: *Conn, socket: std.posix.socket_t, buffer: []const u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.send(&cmd, socket, buffer);
return try cmd.wait();
}
pub fn receive(self: *Conn, socket: std.posix.socket_t, buffer: []u8) !usize {
var cmd = Command{ .impl = NetworkImpl.init(self.loop) };
cmd.impl.receive(&cmd, socket, buffer);
return try cmd.wait();
}
};
pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream {
// TODO async resolve
const list = try net.getAddressList(alloc, name, port);
defer list.deinit();
if (list.addrs.len == 0) return error.UnknownHostName;
for (list.addrs) |addr| {
return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) {
error.ConnectionRefused => {
continue;
},
else => return err,
};
}
return std.posix.ConnectError.ConnectionRefused;
}
pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream {
const sockfd = try std.posix.socket(addr.any.family, std.posix.SOCK.STREAM, std.posix.IPPROTO.TCP);
errdefer std.posix.close(sockfd);
var conn = try alloc.create(Conn);
conn.* = Conn{ .loop = loop };
try conn.connect(sockfd, addr);
return Stream{
.alloc = alloc,
.conn = conn,
.handle = sockfd,
};
}

View File

@@ -1,189 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const http = std.http;
const Client = @import("Client.zig");
const Request = @import("Client.zig").Request;
pub const Loop = @import("jsruntime").Loop;
const url = "https://w3.org";
test "blocking mode fetch API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
const res = try client.fetch(.{
.location = .{ .uri = try std.Uri.parse(url) },
});
try std.testing.expect(res.status == .ok);
}
test "blocking mode open/send/wait API" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client: Client = .{
.allocator = alloc,
.loop = &loop,
};
defer client.deinit();
// force client's CA cert scan from system.
try client.ca_bundle.rescan(client.allocator);
var buf: [2014]u8 = undefined;
var req = try client.open(.GET, try std.Uri.parse(url), .{
.server_header_buffer = &buf,
});
defer req.deinit();
try req.send();
try req.finish();
try req.wait();
try std.testing.expect(req.response.status == .ok);
}
// Example how to write an async http client using the modified standard client.
const AsyncClient = struct {
cli: Client,
const YieldImpl = Loop.Yield(AsyncRequest);
const AsyncRequest = struct {
const State = enum { new, open, send, finish, wait, done };
cli: *Client,
uri: std.Uri,
req: ?Request = undefined,
state: State = .new,
impl: YieldImpl,
err: ?anyerror = null,
buf: [2014]u8 = undefined,
pub fn deinit(self: *AsyncRequest) void {
if (self.req) |*r| r.deinit();
}
pub fn fetch(self: *AsyncRequest) void {
self.state = .new;
return self.impl.yield(self);
}
fn onerr(self: *AsyncRequest, err: anyerror) void {
self.state = .done;
self.err = err;
}
pub fn onYield(self: *AsyncRequest, err: ?anyerror) void {
if (err) |e| return self.onerr(e);
switch (self.state) {
.new => {
self.state = .open;
self.req = self.cli.open(.GET, self.uri, .{
.server_header_buffer = &self.buf,
}) catch |e| return self.onerr(e);
},
.open => {
self.state = .send;
self.req.?.send() catch |e| return self.onerr(e);
},
.send => {
self.state = .finish;
self.req.?.finish() catch |e| return self.onerr(e);
},
.finish => {
self.state = .wait;
self.req.?.wait() catch |e| return self.onerr(e);
},
.wait => {
self.state = .done;
return;
},
.done => return,
}
return self.impl.yield(self);
}
pub fn wait(self: *AsyncRequest) !void {
while (self.state != .done) try self.impl.tick();
if (self.err) |err| return err;
}
};
pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient {
return .{
.cli = .{
.allocator = alloc,
.loop = loop,
},
};
}
pub fn deinit(self: *AsyncClient) void {
self.cli.deinit();
}
pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest {
return .{
.impl = YieldImpl.init(self.cli.loop),
.cli = &self.cli,
.uri = uri,
};
}
};
test "non blocking client" {
const alloc = std.testing.allocator;
var loop = try Loop.init(alloc);
defer loop.deinit();
var client = AsyncClient.init(alloc, &loop);
defer client.deinit();
var reqs: [3]AsyncClient.AsyncRequest = undefined;
for (0..reqs.len) |i| {
reqs[i] = try client.createRequest(try std.Uri.parse(url));
reqs[i].fetch();
}
for (0..reqs.len) |i| {
try reqs[i].wait();
reqs[i].deinit();
}
}

View File

@@ -40,7 +40,7 @@ const storage = @import("../storage/storage.zig");
const FetchResult = @import("../http/Client.zig").Client.FetchResult;
const UserContext = @import("../user_context.zig").UserContext;
const HttpClient = @import("../async/Client.zig");
const HttpClient = @import("asyncio").Client;
const log = std.log.scoped(.browser);
@@ -49,24 +49,29 @@ const log = std.log.scoped(.browser);
// A browser contains only one session.
// TODO allow multiple sessions per browser.
pub const Browser = struct {
session: *Session,
session: Session = undefined,
pub fn init(alloc: std.mem.Allocator, vm: jsruntime.VM) !Browser {
const uri = "about:blank";
pub fn init(self: *Browser, alloc: std.mem.Allocator, loop: *Loop, vm: jsruntime.VM) !void {
// We want to ensure the caller initialised a VM, but the browser
// doesn't use it directly...
_ = vm;
return Browser{
.session = try Session.init(alloc, "about:blank"),
};
try Session.init(&self.session, alloc, loop, uri);
}
pub fn deinit(self: *Browser) void {
self.session.deinit();
}
pub fn currentSession(self: *Browser) *Session {
return self.session;
pub fn newSession(
self: *Browser,
alloc: std.mem.Allocator,
loop: *jsruntime.Loop,
) !void {
self.session.deinit();
try Session.init(&self.session, alloc, loop, uri);
}
};
@@ -90,37 +95,37 @@ pub const Session = struct {
// TODO handle proxy
loader: Loader,
env: Env = undefined,
loop: Loop,
inspector: ?jsruntime.Inspector = null,
window: Window,
// TODO move the shed to the browser?
storageShed: storage.Shed,
page: ?*Page = null,
page: ?Page = null,
httpClient: HttpClient,
jstypes: [Types.len]usize = undefined,
fn init(alloc: std.mem.Allocator, uri: []const u8) !*Session {
var self = try alloc.create(Session);
fn init(self: *Session, alloc: std.mem.Allocator, loop: *Loop, uri: []const u8) !void {
self.* = Session{
.uri = uri,
.alloc = alloc,
.arena = std.heap.ArenaAllocator.init(alloc),
.window = Window.create(null),
.loader = Loader.init(alloc),
.loop = try Loop.init(alloc),
.storageShed = storage.Shed.init(alloc),
.httpClient = undefined,
};
self.env = try Env.init(self.arena.allocator(), &self.loop, null);
self.httpClient = .{ .allocator = alloc, .loop = &self.loop };
Env.init(&self.env, self.arena.allocator(), loop, null);
self.httpClient = .{ .allocator = alloc };
try self.env.load(&self.jstypes);
return self;
}
fn deinit(self: *Session) void {
if (self.page) |page| page.end();
if (self.page) |*p| p.end();
if (self.inspector) |inspector| {
inspector.deinit(self.alloc);
}
self.env.deinit();
self.arena.deinit();
@@ -128,12 +133,35 @@ pub const Session = struct {
self.httpClient.deinit();
self.loader.deinit();
self.storageShed.deinit();
self.loop.deinit();
self.alloc.destroy(self);
}
pub fn createPage(self: *Session) !Page {
return Page.init(self.alloc, self);
pub fn initInspector(
self: *Session,
ctx: anytype,
onResp: jsruntime.InspectorOnResponseFn,
onEvent: jsruntime.InspectorOnEventFn,
) !void {
const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
self.inspector = try jsruntime.Inspector.init(self.alloc, self.env, ctx_opaque, onResp, onEvent);
self.env.setInspector(self.inspector.?);
}
pub fn callInspector(self: *Session, msg: []const u8) void {
if (self.inspector) |inspector| {
inspector.send(msg, self.env);
} else {
@panic("No Inspector");
}
}
// NOTE: the caller is not the owner of the returned value,
// the pointer on Page is just returned as a convenience
pub fn createPage(self: *Session) !*Page {
if (self.page != null) return error.SessionPageExists;
const p: Page = undefined;
self.page = p;
Page.init(&self.page.?, self.alloc, self);
return &self.page.?;
}
};
@@ -155,16 +183,14 @@ pub const Page = struct {
raw_data: ?[]const u8 = null,
fn init(
self: *Page,
alloc: std.mem.Allocator,
session: *Session,
) !Page {
if (session.page != null) return error.SessionPageExists;
var page = Page{
) void {
self.* = .{
.arena = std.heap.ArenaAllocator.init(alloc),
.session = session,
};
session.page = &page;
return page;
}
// reset js env and mem arena.
@@ -219,7 +245,9 @@ pub const Page = struct {
}
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
pub fn navigate(self: *Page, uri: []const u8) !void {
// - auxData: extra data forwarded to the Inspector
// see Inspector.contextCreated
pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void {
const alloc = self.arena.allocator();
log.debug("starting GET {s}", .{uri});
@@ -280,7 +308,7 @@ pub const Page = struct {
log.debug("header content-type: {s}", .{ct.?});
const mime = try Mime.parse(ct.?);
if (mime.eql(Mime.HTML)) {
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8");
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8", auxData);
} else {
log.info("non-HTML document: {s}", .{ct.?});
@@ -290,7 +318,7 @@ pub const Page = struct {
}
// https://html.spec.whatwg.org/#read-html
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void {
const alloc = self.arena.allocator();
// start netsurf memory arena.
@@ -327,6 +355,11 @@ pub const Page = struct {
log.debug("start js env", .{});
try self.session.env.start();
// inspector
if (self.session.inspector) |inspector| {
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
}
// replace the user context document with the new one.
try self.session.env.setUserContext(.{
.document = html_doc,
@@ -411,7 +444,9 @@ pub const Page = struct {
// > immediately before the browser continues to parse the
// > page.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, null);
}
// TODO wait for deferred scripts
@@ -428,7 +463,9 @@ pub const Page = struct {
// eval async scripts.
for (sasync.items) |e| {
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, null);
}
// TODO wait for async scripts
@@ -526,7 +563,7 @@ pub const Page = struct {
const resp = fetchres.req.response;
log.info("fech script {any}: {d}", .{ u, resp.status });
log.info("fetch script {any}: {d}", .{ u, resp.status });
if (resp.status != .ok) return FetchError.BadStatusCode;

148
src/cdp/browser.zig Normal file
View File

@@ -0,0 +1,148 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
getVersion,
setDownloadBehavior,
getWindowForTarget,
setWindowBounds,
};
pub fn browser(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.getVersion => getVersion(alloc, msg, ctx),
.setDownloadBehavior => setDownloadBehavior(alloc, msg, ctx),
.getWindowForTarget => getWindowForTarget(alloc, msg, ctx),
.setWindowBounds => setWindowBounds(alloc, msg, ctx),
};
}
// TODO: hard coded data
const ProtocolVersion = "1.3";
const Product = "Chrome/124.0.6367.29";
const Revision = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
const UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
const JsVersion = "12.4.254.8";
fn getVersion(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getVersion" });
// ouput
const Res = struct {
protocolVersion: []const u8 = ProtocolVersion,
product: []const u8 = Product,
revision: []const u8 = Revision,
userAgent: []const u8 = UserAgent,
jsVersion: []const u8 = JsVersion,
};
return result(alloc, input.id, Res, .{}, null);
}
// TODO: noop method
fn setDownloadBehavior(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
behavior: []const u8,
browserContextId: ?[]const u8 = null,
downloadPath: ?[]const u8 = null,
eventsEnabled: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("REQ > id {d}, method {s}", .{ input.id, "browser.setDownloadBehavior" });
// output
return result(alloc, input.id, null, null, null);
}
// TODO: hard coded ID
const DevToolsWindowID = 1923710101;
fn getWindowForTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
std.debug.assert(input.sessionId != null);
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getWindowForTarget" });
// output
const Resp = struct {
windowId: u64 = DevToolsWindowID,
bounds: struct {
left: ?u64 = null,
top: ?u64 = null,
width: ?u64 = null,
height: ?u64 = null,
windowState: []const u8 = "normal",
} = .{},
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
}
// TODO: noop method
fn setWindowBounds(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.setWindowBounds" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}

211
src/cdp/cdp.zig Normal file
View File

@@ -0,0 +1,211 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const browser = @import("browser.zig").browser;
const target = @import("target.zig").target;
const page = @import("page.zig").page;
const log = @import("log.zig").log;
const runtime = @import("runtime.zig").runtime;
const network = @import("network.zig").network;
const emulation = @import("emulation.zig").emulation;
const fetch = @import("fetch.zig").fetch;
const performance = @import("performance.zig").performance;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const log_cdp = std.log.scoped(.cdp);
pub const Error = error{
UnknonwDomain,
UnknownMethod,
NoResponse,
RequestWithoutID,
};
pub fn isCdpError(err: anyerror) ?Error {
// see https://github.com/ziglang/zig/issues/2473
const errors = @typeInfo(Error).ErrorSet.?;
inline for (errors) |e| {
if (std.mem.eql(u8, e.name, @errorName(err))) {
return @errorCast(err);
}
}
return null;
}
const Domains = enum {
Browser,
Target,
Page,
Log,
Runtime,
Network,
Emulation,
Fetch,
Performance,
};
// The caller is responsible for calling `free` on the returned slice.
pub fn do(
alloc: std.mem.Allocator,
s: []const u8,
ctx: *Ctx,
) ![]const u8 {
// incoming message parser
var msg = IncomingMessage.init(alloc, s);
defer msg.deinit();
const method = try msg.getMethod();
// retrieve domain from method
var iter = std.mem.splitScalar(u8, method, '.');
const domain = std.meta.stringToEnum(Domains, iter.first()) orelse
return error.UnknonwDomain;
// select corresponding domain
const action = iter.next() orelse return error.BadMethod;
return switch (domain) {
.Browser => browser(alloc, &msg, action, ctx),
.Target => target(alloc, &msg, action, ctx),
.Page => page(alloc, &msg, action, ctx),
.Log => log(alloc, &msg, action, ctx),
.Runtime => runtime(alloc, &msg, action, ctx),
.Network => network(alloc, &msg, action, ctx),
.Emulation => emulation(alloc, &msg, action, ctx),
.Fetch => fetch(alloc, &msg, action, ctx),
.Performance => performance(alloc, &msg, action, ctx),
};
}
pub const State = struct {
executionContextId: u32 = 0,
contextID: ?[]const u8 = null,
frameID: []const u8 = FrameID,
url: []const u8 = URLBase,
securityOrigin: []const u8 = URLBase,
secureContextType: []const u8 = "Secure", // TODO: enum
loaderID: []const u8 = LoaderID,
page_life_cycle_events: bool = false, // TODO; Target based value
};
// Utils
// -----
pub fn dumpFile(
alloc: std.mem.Allocator,
id: u16,
script: []const u8,
) !void {
const name = try std.fmt.allocPrint(alloc, "id_{d}.js", .{id});
defer alloc.free(name);
var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
defer dir.close();
const f = try dir.createFile(name, .{});
defer f.close();
const nb = try f.write(script);
std.debug.assert(nb == script.len);
const p = try dir.realpathAlloc(alloc, name);
defer alloc.free(p);
}
// caller owns the slice returned
pub fn stringify(alloc: std.mem.Allocator, res: anytype) ![]const u8 {
var out = std.ArrayList(u8).init(alloc);
defer out.deinit();
// Do not emit optional null fields
const options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false };
try std.json.stringify(res, options, out.writer());
const ret = try alloc.alloc(u8, out.items.len);
@memcpy(ret, out.items);
return ret;
}
const resultNull = "{{\"id\": {d}, \"result\": {{}}}}";
const resultNullSession = "{{\"id\": {d}, \"result\": {{}}, \"sessionId\": \"{s}\"}}";
// caller owns the slice returned
pub fn result(
alloc: std.mem.Allocator,
id: u16,
comptime T: ?type,
res: anytype,
sessionID: ?[]const u8,
) ![]const u8 {
log_cdp.debug(
"Res > id {d}, sessionID {?s}, result {any}",
.{ id, sessionID, res },
);
if (T == null) {
// No need to stringify a custom JSON msg, just use string templates
if (sessionID) |sID| {
return try std.fmt.allocPrint(alloc, resultNullSession, .{ id, sID });
}
return try std.fmt.allocPrint(alloc, resultNull, .{id});
}
const Resp = struct {
id: u16,
result: T.?,
sessionId: ?[]const u8,
};
const resp = Resp{ .id = id, .result = res, .sessionId = sessionID };
return stringify(alloc, resp);
}
pub fn sendEvent(
alloc: std.mem.Allocator,
ctx: *Ctx,
name: []const u8,
comptime T: type,
params: T,
sessionID: ?[]const u8,
) !void {
log_cdp.debug("Event > method {s}, sessionID {?s}", .{ name, sessionID });
const Resp = struct {
method: []const u8,
params: T,
sessionId: ?[]const u8,
};
const resp = Resp{ .method = name, .params = params, .sessionId = sessionID };
const event_msg = try stringify(alloc, resp);
try server.sendAsync(ctx, event_msg);
}
// Common
// ------
// TODO: hard coded IDs
pub const BrowserSessionID = "9559320D92474062597D9875C664CAC0";
pub const ContextSessionID = "4FDC2CB760A23A220497A05C95417CF4";
pub const URLBase = "chrome://newtab/";
pub const FrameID = "90D14BBD8AED408A0467AC93100BCDBE";
pub const LoaderID = "CFC8BED824DD2FD56CF1EF33C965C79C";
pub const TimestampEvent = struct {
timestamp: f64,
};

123
src/cdp/emulation.zig Normal file
View File

@@ -0,0 +1,123 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const stringify = cdp.stringify;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
setEmulatedMedia,
setFocusEmulationEnabled,
setDeviceMetricsOverride,
setTouchEmulationEnabled,
};
pub fn emulation(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.setEmulatedMedia => setEmulatedMedia(alloc, msg, ctx),
.setFocusEmulationEnabled => setFocusEmulationEnabled(alloc, msg, ctx),
.setDeviceMetricsOverride => setDeviceMetricsOverride(alloc, msg, ctx),
.setTouchEmulationEnabled => setTouchEmulationEnabled(alloc, msg, ctx),
};
}
const MediaFeature = struct {
name: []const u8,
value: []const u8,
};
// TODO: noop method
fn setEmulatedMedia(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
media: ?[]const u8 = null,
features: ?[]MediaFeature = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setEmulatedMedia" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setFocusEmulationEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
enabled: bool,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setFocusEmulationEnabled" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setDeviceMetricsOverride(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setDeviceMetricsOverride" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setTouchEmulationEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "emulation.setTouchEmulationEnabled" });
return result(alloc, input.id, null, null, input.sessionId);
}

59
src/cdp/fetch.zig Normal file
View File

@@ -0,0 +1,59 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
disable,
};
pub fn fetch(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.disable => disable(alloc, msg, ctx),
};
}
// TODO: noop method
fn disable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "fetch.disable" });
return result(alloc, input.id, null, null, input.sessionId);
}

59
src/cdp/log.zig Normal file
View File

@@ -0,0 +1,59 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const stringify = cdp.stringify;
const log_cdp = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn log(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log_cdp.debug("Req > id {d}, method {s}", .{ input.id, "log.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

252
src/cdp/msg.zig Normal file
View File

@@ -0,0 +1,252 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
// Parse incoming protocol message in json format.
pub const IncomingMessage = struct {
scanner: std.json.Scanner,
json: []const u8,
obj_begin: bool = false,
obj_end: bool = false,
id: ?u16 = null,
scan_sessionId: bool = false,
sessionId: ?[]const u8 = null,
method: ?[]const u8 = null,
params_skip: bool = false,
pub fn init(alloc: std.mem.Allocator, json: []const u8) IncomingMessage {
return .{
.json = json,
.scanner = std.json.Scanner.initCompleteInput(alloc, json),
};
}
pub fn deinit(self: *IncomingMessage) void {
self.scanner.deinit();
}
fn scanUntil(self: *IncomingMessage, key: []const u8) !void {
while (true) {
switch (try self.scanner.next()) {
.end_of_document => return error.EndOfDocument,
.object_begin => {
if (self.obj_begin) return error.InvalidObjectBegin;
self.obj_begin = true;
},
.object_end => {
if (!self.obj_begin) return error.InvalidObjectEnd;
if (self.obj_end) return error.InvalidObjectEnd;
self.obj_end = true;
},
.string => |s| {
// is the key what we expects?
if (std.mem.eql(u8, s, key)) return;
// save other known keys
if (std.mem.eql(u8, s, "id")) try self.scanId();
if (std.mem.eql(u8, s, "sessionId")) try self.scanSessionId();
if (std.mem.eql(u8, s, "method")) try self.scanMethod();
if (std.mem.eql(u8, s, "params")) try self.scanParams();
// TODO should we skip unknown key?
},
else => return error.InvalidToken,
}
}
}
fn scanId(self: *IncomingMessage) !void {
const t = try self.scanner.next();
if (t != .number) return error.InvalidId;
self.id = try std.fmt.parseUnsigned(u16, t.number, 10);
}
fn getId(self: *IncomingMessage) !u16 {
if (self.id != null) return self.id.?;
try self.scanUntil("id");
try self.scanId();
return self.id.?;
}
fn scanSessionId(self: *IncomingMessage) !void {
switch (try self.scanner.next()) {
// session id can be null.
.null => return,
.string => |s| self.sessionId = s,
else => return error.InvalidSessionId,
}
self.scan_sessionId = true;
}
fn getSessionId(self: *IncomingMessage) !?[]const u8 {
if (self.scan_sessionId) return self.sessionId;
self.scanUntil("sessionId") catch |err| {
if (err != error.EndOfDocument) return err;
// if the document doesn't contains any session id key, we must
// return null value.
self.scan_sessionId = true;
return null;
};
try self.scanSessionId();
return self.sessionId;
}
fn scanMethod(self: *IncomingMessage) !void {
const t = try self.scanner.next();
if (t != .string) return error.InvalidMethod;
self.method = t.string;
}
pub fn getMethod(self: *IncomingMessage) ![]const u8 {
if (self.method != null) return self.method.?;
try self.scanUntil("method");
try self.scanMethod();
return self.method.?;
}
// scanParams skip found parameters b/c if we encounter params *before*
// asking for getParams, we don't know how to parse them.
fn scanParams(self: *IncomingMessage) !void {
const tt = try self.scanner.peekNextTokenType();
if (tt != .object_begin) return error.InvalidParams;
try self.scanner.skipValue();
self.params_skip = true;
}
// getParams restart the JSON parsing
fn getParams(self: *IncomingMessage, alloc: ?std.mem.Allocator, T: type) !T {
if (T == void) return void{};
std.debug.assert(alloc != null); // if T is not void, alloc should not be null
if (self.params_skip) {
// TODO if the params have been skipped, we have to retart the
// parsing from start.
return error.SkippedParams;
}
try self.scanUntil("params");
// parse "params"
const options = std.json.ParseOptions{
.max_value_len = self.scanner.input.len,
.allocate = .alloc_always,
};
return try std.json.innerParse(T, alloc.?, &self.scanner, options);
}
};
pub fn Input(T: type) type {
return struct {
arena: ?*std.heap.ArenaAllocator = null,
id: u16,
params: T,
sessionId: ?[]const u8,
const Self = @This();
pub fn get(alloc: std.mem.Allocator, msg: *IncomingMessage) !Self {
var arena: ?*std.heap.ArenaAllocator = null;
var allocator: ?std.mem.Allocator = null;
if (T != void) {
arena = try alloc.create(std.heap.ArenaAllocator);
arena.?.* = std.heap.ArenaAllocator.init(alloc);
allocator = arena.?.allocator();
}
errdefer {
if (arena) |_arena| {
_arena.deinit();
alloc.destroy(_arena);
}
}
return .{
.arena = arena,
.params = try msg.getParams(allocator, T),
.id = try msg.getId(),
.sessionId = try msg.getSessionId(),
};
}
pub fn deinit(self: Self) void {
if (self.arena) |arena| {
const allocator = arena.child_allocator;
arena.deinit();
allocator.destroy(arena);
}
}
};
}
test "read incoming message" {
const inputs = [_][]const u8{
\\{"id":1,"method":"foo","sessionId":"bar","params":{"bar":"baz"}}
,
\\{"params":{"bar":"baz"},"id":1,"method":"foo","sessionId":"bar"}
,
\\{"sessionId":"bar","params":{"bar":"baz"},"id":1,"method":"foo"}
,
\\{"method":"foo","sessionId":"bar","params":{"bar":"baz"},"id":1}
,
};
for (inputs) |input| {
var msg = IncomingMessage.init(std.testing.allocator, input);
defer msg.deinit();
try std.testing.expectEqual(1, try msg.getId());
try std.testing.expectEqualSlices(u8, "foo", try msg.getMethod());
try std.testing.expectEqualSlices(u8, "bar", (try msg.getSessionId()).?);
const T = struct { bar: []const u8 };
const in = Input(T).get(std.testing.allocator, &msg) catch |err| {
if (err != error.SkippedParams) return err;
// TODO remove this check when params in the beginning is handled.
continue;
};
defer in.deinit();
try std.testing.expectEqualSlices(u8, "baz", in.params.bar);
}
}
test "read incoming message with null session id" {
const inputs = [_][]const u8{
\\{"id":1}
,
\\{"params":{"bar":"baz"},"id":1,"method":"foo"}
,
\\{"sessionId":null,"params":{"bar":"baz"},"id":1,"method":"foo"}
,
};
for (inputs) |input| {
var msg = IncomingMessage.init(std.testing.allocator, input);
defer msg.deinit();
try std.testing.expect(try msg.getSessionId() == null);
try std.testing.expectEqual(1, try msg.getId());
}
}

75
src/cdp/network.zig Normal file
View File

@@ -0,0 +1,75 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
setCacheDisabled,
};
pub fn network(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
.setCacheDisabled => setCacheDisabled(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "network.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn setCacheDisabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "network.setCacheDisabled" });
return result(alloc, input.id, null, null, input.sessionId);
}

452
src/cdp/page.zig Normal file
View File

@@ -0,0 +1,452 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const stringify = cdp.stringify;
const sendEvent = cdp.sendEvent;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Runtime = @import("runtime.zig");
const Methods = enum {
enable,
getFrameTree,
setLifecycleEventsEnabled,
addScriptToEvaluateOnNewDocument,
createIsolatedWorld,
navigate,
};
pub fn page(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
.getFrameTree => getFrameTree(alloc, msg, ctx),
.setLifecycleEventsEnabled => setLifecycleEventsEnabled(alloc, msg, ctx),
.addScriptToEvaluateOnNewDocument => addScriptToEvaluateOnNewDocument(alloc, msg, ctx),
.createIsolatedWorld => createIsolatedWorld(alloc, msg, ctx),
.navigate => navigate(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}
const Frame = struct {
id: []const u8,
loaderId: []const u8,
url: []const u8,
domainAndRegistry: []const u8 = "",
securityOrigin: []const u8,
mimeType: []const u8 = "text/html",
adFrameStatus: struct {
adFrameType: []const u8 = "none",
} = .{},
secureContextType: []const u8,
crossOriginIsolatedContextType: []const u8 = "NotIsolated",
gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
};
fn getFrameTree(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.getFrameTree" });
// output
const FrameTree = struct {
frameTree: struct {
frame: Frame,
},
childFrames: ?[]@This() = null,
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.page.getFrameTree { ");
try writer.writeAll(".frameTree = { ");
try writer.writeAll(".frame = { ");
const frame = self.frameTree.frame;
try writer.writeAll(".id = ");
try std.fmt.formatText(frame.id, "s", options, writer);
try writer.writeAll(", .loaderId = ");
try std.fmt.formatText(frame.loaderId, "s", options, writer);
try writer.writeAll(", .url = ");
try std.fmt.formatText(frame.url, "s", options, writer);
try writer.writeAll(" } } }");
}
};
const frameTree = FrameTree{
.frameTree = .{
.frame = .{
.id = ctx.state.frameID,
.url = ctx.state.url,
.securityOrigin = ctx.state.securityOrigin,
.secureContextType = ctx.state.secureContextType,
.loaderId = ctx.state.loaderID,
},
},
};
return result(alloc, input.id, FrameTree, frameTree, input.sessionId);
}
fn setLifecycleEventsEnabled(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
enabled: bool,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.setLifecycleEventsEnabled" });
ctx.state.page_life_cycle_events = true;
// output
return result(alloc, input.id, null, null, input.sessionId);
}
const LifecycleEvent = struct {
frameId: []const u8,
loaderId: ?[]const u8,
name: []const u8 = undefined,
timestamp: f32 = undefined,
};
// TODO: hard coded method
fn addScriptToEvaluateOnNewDocument(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
source: []const u8,
worldName: ?[]const u8 = null,
includeCommandLineAPI: bool = false,
runImmediately: bool = false,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "page.addScriptToEvaluateOnNewDocument" });
// output
const Res = struct {
identifier: []const u8 = "1",
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.page.addScriptToEvaluateOnNewDocument { ");
try writer.writeAll(".identifier = ");
try std.fmt.formatText(self.identifier, "s", options, writer);
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Res, Res{}, input.sessionId);
}
// TODO: hard coded method
fn createIsolatedWorld(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
frameId: []const u8,
worldName: []const u8,
grantUniveralAccess: bool,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
std.debug.assert(input.sessionId != null);
log.debug("Req > id {d}, method {s}", .{ input.id, "page.createIsolatedWorld" });
// noop executionContextCreated event
try Runtime.executionContextCreated(
alloc,
ctx,
0,
"",
input.params.worldName,
// TODO: hard coded ID
"7102379147004877974.3265385113993241162",
.{
.isDefault = false,
.type = "isolated",
.frameId = input.params.frameId,
},
input.sessionId,
);
// output
const Resp = struct {
executionContextId: u8 = 0,
};
return result(alloc, input.id, Resp, .{}, input.sessionId);
}
fn navigate(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
url: []const u8,
referrer: ?[]const u8 = null,
transitionType: ?[]const u8 = null, // TODO: enum
frameId: ?[]const u8 = null,
referrerPolicy: ?[]const u8 = null, // TODO: enum
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
std.debug.assert(input.sessionId != null);
log.debug("Req > id {d}, method {s}", .{ input.id, "page.navigate" });
// change state
ctx.state.url = input.params.url;
// TODO: hard coded ID
ctx.state.loaderID = "AF8667A203C5392DBE9AC290044AA4C2";
var life_event = LifecycleEvent{
.frameId = ctx.state.frameID,
.loaderId = ctx.state.loaderID,
};
var ts_event: cdp.TimestampEvent = undefined;
// frameStartedLoading event
// TODO: event partially hard coded
const FrameStartedLoading = struct {
frameId: []const u8,
};
const frame_started_loading = FrameStartedLoading{ .frameId = ctx.state.frameID };
try sendEvent(
alloc,
ctx,
"Page.frameStartedLoading",
FrameStartedLoading,
frame_started_loading,
input.sessionId,
);
if (ctx.state.page_life_cycle_events) {
life_event.name = "init";
life_event.timestamp = 343721.796037;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
}
// output
const Resp = struct {
frameId: []const u8,
loaderId: ?[]const u8,
errorText: ?[]const u8 = null,
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.page.navigate.Resp { ");
try writer.writeAll(".frameId = ");
try std.fmt.formatText(self.frameId, "s", options, writer);
if (self.loaderId) |loaderId| {
try writer.writeAll(", .loaderId = '");
try std.fmt.formatText(loaderId, "s", options, writer);
}
try writer.writeAll(" }");
}
};
const resp = Resp{
.frameId = ctx.state.frameID,
.loaderId = ctx.state.loaderID,
};
const res = try result(alloc, input.id, Resp, resp, input.sessionId);
try server.sendAsync(ctx, res);
// TODO: at this point do we need async the following actions to be async?
// Send Runtime.executionContextsCleared event
// TODO: noop event, we have no env context at this point, is it necesarry?
try sendEvent(alloc, ctx, "Runtime.executionContextsCleared", void, {}, input.sessionId);
// Launch navigate
const p = try ctx.browser.session.createPage();
ctx.state.executionContextId += 1;
const auxData = try std.fmt.allocPrint(
alloc,
// NOTE: we assume this is the default web page
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{ctx.state.frameID},
);
defer alloc.free(auxData);
try p.navigate(input.params.url, auxData);
// Events
// lifecycle init event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
life_event.name = "init";
life_event.timestamp = 343721.796037;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
}
// frameNavigated event
const FrameNavigated = struct {
frame: Frame,
type: []const u8 = "Navigation",
};
const frame_navigated = FrameNavigated{
.frame = .{
.id = ctx.state.frameID,
.url = ctx.state.url,
.securityOrigin = ctx.state.securityOrigin,
.secureContextType = ctx.state.secureContextType,
.loaderId = ctx.state.loaderID,
},
};
try sendEvent(
alloc,
ctx,
"Page.frameNavigated",
FrameNavigated,
frame_navigated,
input.sessionId,
);
// domContentEventFired event
// TODO: partially hard coded
ts_event = .{ .timestamp = 343721.803338 };
try sendEvent(
alloc,
ctx,
"Page.domContentEventFired",
cdp.TimestampEvent,
ts_event,
input.sessionId,
);
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
life_event.name = "DOMContentLoaded";
life_event.timestamp = 343721.803338;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
}
// loadEventFired event
// TODO: partially hard coded
ts_event = .{ .timestamp = 343721.824655 };
try sendEvent(
alloc,
ctx,
"Page.loadEventFired",
cdp.TimestampEvent,
ts_event,
input.sessionId,
);
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (ctx.state.page_life_cycle_events) {
life_event.name = "load";
life_event.timestamp = 343721.824655;
try sendEvent(
alloc,
ctx,
"Page.lifecycleEvent",
LifecycleEvent,
life_event,
input.sessionId,
);
}
// frameStoppedLoading
const FrameStoppedLoading = struct { frameId: []const u8 };
try sendEvent(
alloc,
ctx,
"Page.frameStoppedLoading",
FrameStoppedLoading,
.{ .frameId = ctx.state.frameID },
input.sessionId,
);
return "";
}

59
src/cdp/performance.zig Normal file
View File

@@ -0,0 +1,59 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn performance(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "performance.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

181
src/cdp/runtime.zig Normal file
View File

@@ -0,0 +1,181 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const stringify = cdp.stringify;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
runIfWaitingForDebugger,
evaluate,
addBinding,
callFunctionOn,
releaseObject,
};
pub fn runtime(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
// NOTE: we could send it anyway to the JS runtime but it's good to check it
return error.UnknownMethod;
return switch (method) {
.runIfWaitingForDebugger => runIfWaitingForDebugger(alloc, msg, ctx),
else => sendInspector(alloc, method, msg, ctx),
};
}
fn sendInspector(
alloc: std.mem.Allocator,
method: Methods,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// save script in file at debug mode
if (std.log.defaultLogEnabled(.debug)) {
// input
var id: u16 = undefined;
var script: ?[]const u8 = null;
if (method == .evaluate) {
const Params = struct {
expression: []const u8,
contextId: ?u8 = null,
returnByValue: ?bool = null,
awaitPromise: ?bool = null,
userGesture: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.evaluate" });
const params = input.params;
const func = try alloc.alloc(u8, params.expression.len);
@memcpy(func, params.expression);
script = func;
id = input.id;
} else if (method == .callFunctionOn) {
const Params = struct {
functionDeclaration: []const u8,
objectId: ?[]const u8 = null,
executionContextId: ?u8 = null,
arguments: ?[]struct {
value: ?[]const u8 = null,
objectId: ?[]const u8 = null,
} = null,
returnByValue: ?bool = null,
awaitPromise: ?bool = null,
userGesture: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} (script saved on cache)", .{ input.id, "runtime.callFunctionOn" });
const params = input.params;
const func = try alloc.alloc(u8, params.functionDeclaration.len);
@memcpy(func, params.functionDeclaration);
script = func;
id = input.id;
}
if (script) |src| {
try cdp.dumpFile(alloc, id, src);
alloc.free(src);
}
}
// remove awaitPromise true params
// TODO: delete when Promise are correctly handled by zig-js-runtime
if (method == .callFunctionOn or method == .evaluate) {
const buf = try alloc.alloc(u8, msg.json.len + 1);
defer alloc.free(buf);
_ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
ctx.sendInspector(buf);
} else {
ctx.sendInspector(msg.json);
}
return "";
}
pub const AuxData = struct {
isDefault: bool = true,
type: []const u8 = "default",
frameId: []const u8 = cdp.FrameID,
};
pub fn executionContextCreated(
alloc: std.mem.Allocator,
ctx: *Ctx,
id: u16,
origin: []const u8,
name: []const u8,
uniqueID: []const u8,
auxData: ?AuxData,
sessionID: ?[]const u8,
) !void {
const Params = struct {
context: struct {
id: u64,
origin: []const u8,
name: []const u8,
uniqueId: []const u8,
auxData: ?AuxData = null,
},
};
const params = Params{
.context = .{
.id = id,
.origin = origin,
.name = name,
.uniqueId = uniqueID,
.auxData = auxData,
},
};
try cdp.sendEvent(alloc, ctx, "Runtime.executionContextCreated", Params, params, sessionID);
}
// TODO: noop method
// should we be passing this also to the JS Inspector?
fn runIfWaitingForDebugger(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "runtime.runIfWaitingForDebugger" });
return result(alloc, input.id, null, null, input.sessionId);
}

414
src/cdp/target.zig Normal file
View File

@@ -0,0 +1,414 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const stringify = cdp.stringify;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
setDiscoverTargets,
setAutoAttach,
attachToTarget,
getTargetInfo,
getBrowserContexts,
createBrowserContext,
disposeBrowserContext,
createTarget,
closeTarget,
};
pub fn target(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.setDiscoverTargets => setDiscoverTargets(alloc, msg, ctx),
.setAutoAttach => setAutoAttach(alloc, msg, ctx),
.attachToTarget => attachToTarget(alloc, msg, ctx),
.getTargetInfo => getTargetInfo(alloc, msg, ctx),
.getBrowserContexts => getBrowserContexts(alloc, msg, ctx),
.createBrowserContext => createBrowserContext(alloc, msg, ctx),
.disposeBrowserContext => disposeBrowserContext(alloc, msg, ctx),
.createTarget => createTarget(alloc, msg, ctx),
.closeTarget => closeTarget(alloc, msg, ctx),
};
}
// TODO: hard coded IDs
const PageTargetID = "CFCD6EC01573CF29BB638E9DC0F52DDC";
const BrowserTargetID = "2d2bdef9-1c95-416f-8c0e-83f3ab73a30c";
const BrowserContextID = "65618675CB7D3585A95049E9DFE95EA9";
// TODO: noop method
fn setDiscoverTargets(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.setDiscoverTargets" });
// output
return result(alloc, input.id, null, null, input.sessionId);
}
const AttachToTarget = struct {
sessionId: []const u8,
targetInfo: struct {
targetId: []const u8,
type: []const u8 = "page",
title: []const u8,
url: []const u8,
attached: bool = true,
canAccessOpener: bool = false,
browserContextId: []const u8,
},
waitingForDebugger: bool = false,
};
const TargetFilter = struct {
type: ?[]const u8 = null,
exclude: ?bool = null,
};
// TODO: noop method
fn setAutoAttach(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
autoAttach: bool,
waitForDebuggerOnStart: bool,
flatten: bool = true,
filter: ?[]TargetFilter = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.setAutoAttach" });
// attachedToTarget event
if (input.sessionId == null) {
const attached = AttachToTarget{
.sessionId = cdp.BrowserSessionID,
.targetInfo = .{
.targetId = PageTargetID,
.title = "New Incognito tab",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
},
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
}
// output
return result(alloc, input.id, null, null, input.sessionId);
}
// TODO: noop method
fn attachToTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: []const u8,
flatten: bool = true,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.attachToTarget" });
// attachedToTarget event
if (input.sessionId == null) {
const attached = AttachToTarget{
.sessionId = cdp.BrowserSessionID,
.targetInfo = .{
.targetId = PageTargetID,
.title = "New Incognito tab",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
},
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, null);
}
// output
const SessionId = struct {
sessionId: []const u8,
};
const output = SessionId{
.sessionId = input.sessionId orelse BrowserContextID,
};
return result(alloc, input.id, SessionId, output, null);
}
fn getTargetInfo(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getTargetInfo" });
// output
const TargetInfo = struct {
targetId: []const u8,
type: []const u8,
title: []const u8 = "",
url: []const u8 = "",
attached: bool = true,
openerId: ?[]const u8 = null,
canAccessOpener: bool = false,
openerFrameId: ?[]const u8 = null,
browserContextId: ?[]const u8 = null,
subtype: ?[]const u8 = null,
};
const targetInfo = TargetInfo{
.targetId = BrowserTargetID,
.type = "browser",
};
return result(alloc, input.id, TargetInfo, targetInfo, null);
}
// Browser context are not handled and not in the roadmap for now
// The following methods are "fake"
// TODO: noop method
fn getBrowserContexts(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getBrowserContexts" });
// ouptut
const Resp = struct {
browserContextIds: [][]const u8,
};
var resp: Resp = undefined;
if (ctx.state.contextID) |contextID| {
var contextIDs = [1][]const u8{contextID};
resp = .{ .browserContextIds = &contextIDs };
} else {
const contextIDs = [0][]const u8{};
resp = .{ .browserContextIds = &contextIDs };
}
return result(alloc, input.id, Resp, resp, null);
}
const ContextID = "22648B09EDCCDD11109E2D4FEFBE4F89";
// TODO: noop method
fn createBrowserContext(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
disposeOnDetach: bool = false,
proxyServer: ?[]const u8 = null,
proxyBypassList: ?[]const u8 = null,
originsWithUniversalNetworkAccess: ?[][]const u8 = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.createBrowserContext" });
ctx.state.contextID = ContextID;
// output
const Resp = struct {
browserContextId: []const u8 = ContextID,
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.target.createBrowserContext { ");
try writer.writeAll(".browserContextId = ");
try std.fmt.formatText(self.browserContextId, "s", options, writer);
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
}
fn disposeBrowserContext(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
browserContextId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.disposeBrowserContext" });
// output
const res = try result(alloc, input.id, null, .{}, null);
try server.sendAsync(ctx, res);
return error.DisposeBrowserContext;
}
// TODO: hard coded IDs
const TargetID = "57356548460A8F29706A2ADF14316298";
const LoaderID = "DD4A76F842AA389647D702B4D805F49A";
fn createTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
url: []const u8,
width: ?u64 = null,
height: ?u64 = null,
browserContextId: ?[]const u8 = null,
enableBeginFrameControl: bool = false,
newWindow: bool = false,
background: bool = false,
forTab: ?bool = null,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.createTarget" });
// change CDP state
ctx.state.frameID = TargetID;
ctx.state.url = "about:blank";
ctx.state.securityOrigin = "://";
ctx.state.secureContextType = "InsecureScheme";
ctx.state.loaderID = LoaderID;
// send attachToTarget event
const attached = AttachToTarget{
.sessionId = cdp.ContextSessionID,
.targetInfo = .{
.targetId = ctx.state.frameID,
.title = "",
.url = ctx.state.url,
.browserContextId = input.params.browserContextId orelse ContextID,
},
.waitingForDebugger = true,
};
try cdp.sendEvent(alloc, ctx, "Target.attachedToTarget", AttachToTarget, attached, input.sessionId);
// output
const Resp = struct {
targetId: []const u8 = TargetID,
pub fn format(
self: @This(),
comptime _: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
try writer.writeAll("cdp.target.createTarget { ");
try writer.writeAll(".targetId = ");
try std.fmt.formatText(self.targetId, "s", options, writer);
try writer.writeAll(" }");
}
};
return result(alloc, input.id, Resp, Resp{}, input.sessionId);
}
fn closeTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
targetId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.closeTarget" });
// output
const Resp = struct {
success: bool = true,
};
const res = try result(alloc, input.id, Resp, Resp{}, null);
try server.sendAsync(ctx, res);
// Inspector.detached event
const InspectorDetached = struct {
reason: []const u8 = "Render process gone.",
};
try cdp.sendEvent(
alloc,
ctx,
"Inspector.detached",
InspectorDetached,
.{},
input.sessionId orelse cdp.ContextSessionID,
);
// detachedFromTarget event
const TargetDetached = struct {
sessionId: []const u8,
targetId: []const u8,
};
try cdp.sendEvent(
alloc,
ctx,
"Target.detachedFromTarget",
TargetDetached,
.{
.sessionId = input.sessionId orelse cdp.ContextSessionID,
.targetId = input.params.targetId,
},
null,
);
return "";
}

View File

@@ -29,7 +29,6 @@ const Node = @import("node.zig").Node;
const NodeList = @import("nodelist.zig").NodeList;
const NodeUnion = @import("node.zig").Union;
const Walker = @import("walker.zig").WalkerDepthFirst;
const collection = @import("html_collection.zig");
const css = @import("css.zig");
@@ -435,7 +434,11 @@ pub fn testExecFn(
.{ .src = "document.querySelector(':root').nodeName", .ex = "HTML" },
.{ .src = "document.querySelectorAll('p').length", .ex = "2" },
.{ .src = "document.querySelectorAll('.ok').item(0).id", .ex = "link" },
.{ .src =
\\Array.from(document.querySelectorAll('#content > p#para-empty'))
\\.map(row => row.querySelector('span').textContent)
\\.length;
, .ex = "1" },
};
try checkCases(js_env, &querySelector);

View File

@@ -23,7 +23,7 @@ const EventTarget = @import("event_target.zig").EventTarget;
const DOMImplementation = @import("implementation.zig").DOMImplementation;
const NamedNodeMap = @import("namednodemap.zig").NamedNodeMap;
const DOMTokenList = @import("token_list.zig").DOMTokenList;
const NodeList = @import("nodelist.zig").NodeList;
const NodeList = @import("nodelist.zig");
const Nod = @import("node.zig");
const MutationObserver = @import("mutation_observer.zig");
@@ -33,7 +33,7 @@ pub const Interfaces = generate.Tuple(.{
DOMImplementation,
NamedNodeMap,
DOMTokenList,
NodeList,
NodeList.Interfaces,
Nod.Node,
Nod.Interfaces,
MutationObserver.Interfaces,

View File

@@ -40,6 +40,7 @@ const DocumentType = @import("document_type.zig").DocumentType;
const DocumentFragment = @import("document_fragment.zig").DocumentFragment;
const HTMLCollection = @import("html_collection.zig").HTMLCollection;
const HTMLCollectionIterator = @import("html_collection.zig").HTMLCollectionIterator;
const Walker = @import("walker.zig").WalkerDepthFirst;
// HTML
const HTML = @import("../html/html.zig");
@@ -195,21 +196,68 @@ pub const Node = struct {
return try Node.toInterface(clone);
}
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) void {
// TODO
_ = other;
_ = self;
std.log.err("Not implemented {s}", .{"node.compareDocumentPosition()"});
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
if (self == other) return 0;
const docself = try parser.nodeOwnerDocument(self);
const docother = try parser.nodeOwnerDocument(other);
// Both are in different document.
if (docself == null or docother == null or docother.? != docself.?) {
return @intFromEnum(parser.DocumentPosition.disconnected);
}
// TODO Both are in a different trees in the same document.
const w = Walker{};
var next: ?*parser.Node = null;
// Is other a descendant of self?
while (true) {
next = try w.get_next(self, next) orelse break;
if (other == next) {
return @intFromEnum(parser.DocumentPosition.following) +
@intFromEnum(parser.DocumentPosition.contained_by);
}
}
// Is self a descendant of other?
next = null;
while (true) {
next = try w.get_next(other, next) orelse break;
if (self == next) {
return @intFromEnum(parser.DocumentPosition.contains) +
@intFromEnum(parser.DocumentPosition.preceding);
}
}
next = null;
while (true) {
next = try w.get_next(parser.documentToNode(docself.?), next) orelse break;
if (other == next) {
// other precedes self.
return @intFromEnum(parser.DocumentPosition.preceding);
}
if (self == next) {
// other follows self.
return @intFromEnum(parser.DocumentPosition.following);
}
}
return 0;
}
pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
return try parser.nodeContains(self, other);
}
pub fn _getRootNode(self: *parser.Node) void {
// TODO
_ = self;
std.log.err("Not implemented {s}", .{"node.getRootNode()"});
pub fn _getRootNode(self: *parser.Node) !?HTMLElem.Union {
// TODO return thiss shadow-including root if options["composed"] is true
const res = try parser.nodeOwnerDocument(self);
if (res == null) {
return null;
}
return try HTMLElem.toInterface(HTMLElem.Union, @as(*parser.Element, @ptrCast(res.?)));
}
pub fn _hasChildNodes(self: *parser.Node) !bool {
@@ -384,6 +432,21 @@ pub fn testExecFn(
;
try runScript(js_env, alloc, trim_and_replace, "proto_test");
var node_compare_document_position = [_]Case{
.{ .src = "document.body.compareDocumentPosition(document.firstChild); ", .ex = "10" },
.{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"content\"));", .ex = "10" },
.{ .src = "document.getElementById(\"content\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "20" },
.{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "0" },
.{ .src = "document.getElementById(\"para-empty\").compareDocumentPosition(document.getElementById(\"link\"));", .ex = "2" },
.{ .src = "document.getElementById(\"link\").compareDocumentPosition(document.getElementById(\"para-empty\"));", .ex = "4" },
};
try checkCases(js_env, &node_compare_document_position);
var get_root_node = [_]Case{
.{ .src = "document.getElementById('content').getRootNode().__proto__.constructor.name", .ex = "HTMLDocument" },
};
try checkCases(js_env, &get_root_node);
var first_child = [_]Case{
// for next test cases
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },

View File

@@ -21,14 +21,82 @@ const std = @import("std");
const parser = @import("netsurf");
const jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const CallbackResult = jsruntime.CallbackResult;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const NodeUnion = @import("node.zig").Union;
const Node = @import("node.zig").Node;
const U32Iterator = @import("../iterator/iterator.zig").U32Iterator;
const log = std.log.scoped(.nodelist);
const DOMException = @import("exceptions.zig").DOMException;
pub const Interfaces = generate.Tuple(.{
NodeListIterator,
NodeList,
});
pub const NodeListIterator = struct {
pub const mem_guarantied = true;
coll: *NodeList,
index: u32 = 0,
pub const Return = struct {
value: ?NodeUnion,
done: bool,
};
pub fn _next(self: *NodeListIterator) !Return {
const e = try self.coll._item(self.index);
if (e == null) {
return Return{
.value = null,
.done = true,
};
}
self.index += 1;
return Return{
.value = e,
.done = false,
};
}
};
pub const NodeListEntriesIterator = struct {
pub const mem_guarantied = true;
coll: *NodeList,
index: u32 = 0,
pub const Return = struct {
value: ?NodeUnion,
done: bool,
};
pub fn _next(self: *NodeListEntriesIterator) !Return {
const e = try self.coll._item(self.index);
if (e == null) {
return Return{
.value = null,
.done = true,
};
}
self.index += 1;
return Return{
.value = e,
.done = false,
};
}
};
// Nodelist is implemented in pure Zig b/c libdom's NodeList doesn't allow to
// append nodes.
// WEB IDL https://dom.spec.whatwg.org/#nodelist
@@ -72,9 +140,50 @@ pub const NodeList = struct {
return try Node.toInterface(n);
}
// TODO _symbol_iterator
pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg
var res = CallbackResult.init(alloc);
defer res.deinit();
// TODO implement postAttach
for (self.nodes.items, 0..) |n, i| {
const ii: u32 = @intCast(i);
cbk.trycall(.{ n, ii, self }, &res) catch |e| {
log.err("callback error: {s}", .{res.result orelse "unknown"});
log.debug("{s}", .{res.stack orelse "no stack trace"});
return e;
};
}
}
pub fn _keys(self: *NodeList) U32Iterator {
return .{
.length = self.get_length(),
};
}
pub fn _values(self: *NodeList) NodeListIterator {
return .{
.coll = self,
};
}
pub fn _symbol_iterator(self: *NodeList) NodeListIterator {
return self._values();
}
// TODO entries() https://developer.mozilla.org/en-US/docs/Web/API/NodeList/entries
pub fn postAttach(self: *NodeList, alloc: std.mem.Allocator, js_obj: jsruntime.JSObject) !void {
const ln = self.get_length();
var i: u32 = 0;
while (i < ln) {
defer i += 1;
const k = try std.fmt.allocPrint(alloc, "{d}", .{i});
const node = try self._item(i) orelse unreachable;
try js_obj.set(k, node);
}
}
};
// Tests
@@ -87,6 +196,14 @@ pub fn testExecFn(
var childnodes = [_]Case{
.{ .src = "let list = document.getElementById('content').childNodes", .ex = "undefined" },
.{ .src = "list.length", .ex = "9" },
.{ .src = "list[0].__proto__.constructor.name", .ex = "Text" },
.{ .src =
\\let i = 0;
\\list.forEach(function (n, idx) {
\\ i += idx;
\\});
\\i;
, .ex = "36" },
};
try checkCases(js_env, &childnodes);
}

View File

@@ -251,12 +251,12 @@ pub const EventHandler = struct {
Event.toInterface(evt) catch unreachable,
}, &res) catch |e| log.err("event handler error: {any}", .{e});
} else {
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error: {any}", .{e});
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e});
}
// in case of function error, we log the result and the trace.
if (!res.success) {
log.info("event handler error: {s}", .{res.result orelse "unknown"});
log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
log.debug("{s}", .{res.stack orelse "no stack trace"});
}
}

View File

@@ -153,8 +153,8 @@ pub const HTMLDocument = struct {
return try collection.HTMLCollectionAll(parser.documentHTMLToNode(self), true);
}
pub fn get_currentScript(_: *parser.DocumentHTML) !?*parser.Element {
return null;
pub fn get_currentScript(self: *parser.DocumentHTML) !?*parser.Script {
return try parser.documentHTMLGetCurrentScript(self);
}
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {

View File

@@ -96,6 +96,7 @@ pub const Interfaces = .{
HTMLTrackElement,
HTMLUListElement,
HTMLVideoElement,
CSSProperties,
};
const Generated = generate.Union.compile(Interfaces);
pub const Union = Generated._union;
@@ -104,10 +105,18 @@ pub const Tags = Generated._enum;
// Abstract class
// --------------
const CSSProperties = struct {
pub const mem_guarantied = true;
};
pub const HTMLElement = struct {
pub const Self = parser.ElementHTML;
pub const prototype = *Element;
pub const mem_guarantied = true;
pub fn get_style(_: *parser.ElementHTML) CSSProperties {
return .{};
}
};
// Deprecated HTMLElements in Chrome (2023/03/15)

View File

@@ -28,6 +28,8 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget;
const storage = @import("../storage/storage.zig");
const log = std.log.scoped(.window);
// https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct {
@@ -66,6 +68,10 @@ pub const Window = struct {
return self;
}
pub fn _debug(_: *Window, str: []const u8) void {
log.debug("{s}", .{str});
}
pub fn get_self(self: *Window) *Window {
return self;
}

View File

@@ -1,21 +1,3 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! HTTP(S) Client implementation.
//!
//! Connections are opened in a thread-safe manner, but individual Requests are not.

35
src/iterator/iterator.zig Normal file
View File

@@ -0,0 +1,35 @@
const std = @import("std");
const generate = @import("../generate.zig");
pub const Interfaces = generate.Tuple(.{
U32Iterator,
});
pub const U32Iterator = struct {
pub const mem_guarantied = true;
length: u32,
index: u32 = 0,
pub const Return = struct {
value: u32,
done: bool,
};
pub fn _next(self: *U32Iterator) !Return {
const i = self.index;
if (i >= self.length) {
return Return{
.value = 0,
.done = true,
};
}
self.index += 1;
return Return{
.value = i,
.done = false,
};
}
};

View File

@@ -17,97 +17,367 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const posix = std.posix;
const builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const Browser = @import("browser/browser.zig").Browser;
const server = @import("server.zig");
const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
const socket_path = "/tmp/browsercore-server.sock";
const log = std.log.scoped(.cli);
var doc: *parser.DocumentHTML = undefined;
var server: std.net.Server = undefined;
// Inspired by std.net.StreamServer in Zig < 0.12
pub const StreamServer = struct {
/// Copied from `Options` on `init`.
kernel_backlog: u31,
reuse_address: bool,
reuse_port: bool,
nonblocking: bool,
fn execJS(
alloc: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
// start JS env
try js_env.start();
defer js_env.stop();
/// `undefined` until `listen` returns successfully.
listen_address: std.net.Address,
// alias global as self and window
var window = Window.create(null);
window.replaceDocument(doc);
try js_env.bindGlobal(window);
sockfd: ?posix.socket_t,
// try catch
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(js_env.*);
defer try_catch.deinit();
pub const Options = struct {
/// How many connections the kernel will accept on the application's behalf.
/// If more than this many connections pool in the kernel, clients will start
/// seeing "Connection refused".
kernel_backlog: u31 = 128,
while (true) {
/// Enable SO.REUSEADDR on the socket.
reuse_address: bool = false,
// read cmd
const conn = try server.accept();
var buf: [100]u8 = undefined;
const read = try conn.stream.read(&buf);
const cmd = buf[0..read];
std.debug.print("<- {s}\n", .{cmd});
if (std.mem.eql(u8, cmd, "exit")) {
break;
/// Enable SO.REUSEPORT on the socket.
reuse_port: bool = false,
/// Non-blocking mode.
nonblocking: bool = false,
};
/// After this call succeeds, resources have been acquired and must
/// be released with `deinit`.
pub fn init(options: Options) StreamServer {
return StreamServer{
.sockfd = null,
.kernel_backlog = options.kernel_backlog,
.reuse_address = options.reuse_address,
.reuse_port = options.reuse_port,
.nonblocking = options.nonblocking,
.listen_address = undefined,
};
}
/// Release all resources. The `StreamServer` memory becomes `undefined`.
pub fn deinit(self: *StreamServer) void {
self.close();
self.* = undefined;
}
fn setSockOpt(fd: posix.socket_t, level: i32, option: u32, value: c_int) !void {
try posix.setsockopt(fd, level, option, &std.mem.toBytes(value));
}
pub fn listen(self: *StreamServer, address: std.net.Address) !void {
const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
var use_sock_flags: u32 = sock_flags;
if (self.nonblocking) use_sock_flags |= posix.SOCK.NONBLOCK;
const proto = if (address.any.family == posix.AF.UNIX) @as(u32, 0) else posix.IPPROTO.TCP;
const sockfd = try posix.socket(address.any.family, use_sock_flags, proto);
self.sockfd = sockfd;
errdefer {
posix.close(sockfd);
self.sockfd = null;
}
const res = try js_env.exec(cmd, "cdp");
const res_str = try res.toString(alloc, js_env.*);
defer alloc.free(res_str);
std.debug.print("-> {s}\n", .{res_str});
// socket options
if (self.reuse_address) {
try setSockOpt(sockfd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1);
}
if (@hasDecl(posix.SO, "REUSEPORT") and self.reuse_port) {
try setSockOpt(sockfd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1);
}
if (builtin.target.os.tag == .linux) { // posix.TCP not available on MacOS
// WARNING: disable Nagle's alogrithm to avoid latency issues
try setSockOpt(sockfd, posix.IPPROTO.TCP, posix.TCP.NODELAY, 1);
}
_ = try conn.stream.write(res_str);
var socklen = address.getOsSockLen();
try posix.bind(sockfd, &address.any, socklen);
try posix.listen(sockfd, self.kernel_backlog);
try posix.getsockname(sockfd, &self.listen_address.any, &socklen);
}
/// Stop listening. It is still necessary to call `deinit` after stopping listening.
/// Calling `deinit` will automatically call `close`. It is safe to call `close` when
/// not listening.
pub fn close(self: *StreamServer) void {
if (self.sockfd) |fd| {
posix.close(fd);
self.sockfd = null;
self.listen_address = undefined;
}
}
};
const usage =
\\usage: {s} [options] [URL]
\\
\\ start Lightpanda browser
\\
\\ * if an url is provided the browser will fetch the page and exit
\\ * otherwhise the browser starts a CDP server
\\
\\ -h, --help Print this help message and exit.
\\ --host Host of the CDP server (default "127.0.0.1")
\\ --port Port of the CDP server (default "3245")
\\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
\\ --dump Dump document in stdout (fetch mode only)
\\
;
fn printUsageExit(execname: []const u8, res: u8) anyerror {
std.io.getStdErr().writer().print(usage, .{execname}) catch |err| {
std.log.err("Print usage error: {any}", .{err});
return error.Cli;
};
if (res == 1) return error.Usage;
return error.NoError;
}
const CliModeTag = enum {
server,
fetch,
};
const CliMode = union(CliModeTag) {
server: Server,
fetch: Fetch,
const Server = struct {
execname: []const u8 = undefined,
args: *std.process.ArgIterator = undefined,
addr: std.net.Address = undefined,
host: []const u8 = Host,
port: u16 = Port,
timeout: u8 = Timeout,
// default options
const Host = "127.0.0.1";
const Port = 3245;
const Timeout = 3; // in seconds
};
const Fetch = struct {
execname: []const u8 = undefined,
args: *std.process.ArgIterator = undefined,
url: []const u8 = "",
dump: bool = false,
};
fn init(alloc: std.mem.Allocator, args: *std.process.ArgIterator) !CliMode {
args.* = try std.process.argsWithAllocator(alloc);
errdefer args.deinit();
const execname = args.next().?;
var default_mode: CliModeTag = .server;
var _server = Server{};
var _fetch = Fetch{};
while (args.next()) |opt| {
if (std.mem.eql(u8, "-h", opt) or std.mem.eql(u8, "--help", opt)) {
return printUsageExit(execname, 0);
}
if (std.mem.eql(u8, "--dump", opt)) {
_fetch.dump = true;
continue;
}
if (std.mem.eql(u8, "--host", opt)) {
if (args.next()) |arg| {
_server.host = arg;
continue;
} else {
std.log.err("--host not provided\n", .{});
return printUsageExit(execname, 1);
}
}
if (std.mem.eql(u8, "--port", opt)) {
if (args.next()) |arg| {
_server.port = std.fmt.parseInt(u16, arg, 10) catch |err| {
log.err("--port {any}\n", .{err});
return printUsageExit(execname, 1);
};
continue;
} else {
log.err("--port not provided\n", .{});
return printUsageExit(execname, 1);
}
}
if (std.mem.eql(u8, "--timeout", opt)) {
if (args.next()) |arg| {
_server.timeout = std.fmt.parseInt(u8, arg, 10) catch |err| {
log.err("--timeout {any}\n", .{err});
return printUsageExit(execname, 1);
};
continue;
} else {
log.err("--timeout not provided\n", .{});
return printUsageExit(execname, 1);
}
}
// unknown option
if (std.mem.startsWith(u8, opt, "--")) {
log.err("unknown option\n", .{});
return printUsageExit(execname, 1);
}
// other argument is considered to be an URL, ie. fetch mode
default_mode = .fetch;
// allow only one url
if (_fetch.url.len != 0) {
log.err("more than 1 url provided\n", .{});
return printUsageExit(execname, 1);
}
_fetch.url = opt;
}
if (default_mode == .server) {
// server mode
_server.addr = std.net.Address.parseIp4(_server.host, _server.port) catch |err| {
log.err("address (host:port) {any}\n", .{err});
return printUsageExit(execname, 1);
};
_server.execname = execname;
_server.args = args;
return CliMode{ .server = _server };
} else {
// fetch mode
_fetch.execname = execname;
_fetch.args = args;
return CliMode{ .fetch = _fetch };
}
}
fn deinit(self: CliMode) void {
switch (self) {
inline .server, .fetch => |*_mode| {
_mode.args.deinit();
},
}
}
};
pub fn main() !void {
// create v8 vm
const vm = jsruntime.VM.init();
defer vm.deinit();
// alloc
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
try parser.init();
defer parser.deinit();
// document
const file = try std.fs.cwd().openFile("test.html", .{});
defer file.close();
doc = try parser.documentHTMLParse(file.reader(), "UTF-8");
defer parser.documentHTMLClose(doc) catch |err| {
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
};
// remove socket file of internal server
// reuse_address (SO_REUSEADDR flag) does not seems to work on unix socket
// see: https://gavv.net/articles/unix-socket-reuse/
// TODO: use a lock file instead
std.posix.unlink(socket_path) catch |err| {
if (err != error.FileNotFound) {
return err;
// allocator
// - in Debug mode we use the General Purpose Allocator to detect memory leaks
// - in Release mode we use the page allocator
var alloc: std.mem.Allocator = undefined;
var _gpa: ?std.heap.GeneralPurposeAllocator(.{}) = null;
if (builtin.mode == .Debug) {
_gpa = std.heap.GeneralPurposeAllocator(.{}){};
alloc = _gpa.?.allocator();
} else {
alloc = std.heap.page_allocator;
}
defer {
if (_gpa) |*gpa| {
switch (gpa.deinit()) {
.ok => std.debug.print("No memory leaks\n", .{}),
.leak => @panic("Memory leak"),
}
}
}
// args
var args: std.process.ArgIterator = undefined;
const cli_mode = CliMode.init(alloc, &args) catch |err| {
if (err == error.NoError) {
std.posix.exit(0);
} else {
std.posix.exit(1);
}
return;
};
defer cli_mode.deinit();
// server
const addr = try std.net.Address.initUnix(socket_path);
server = try addr.listen(.{});
defer server.deinit();
std.debug.print("Listening on: {s}...\n", .{socket_path});
switch (cli_mode) {
.server => |mode| {
try jsruntime.loadEnv(&arena, null, execJS);
// server
var srv = StreamServer.init(.{
.reuse_address = true,
.reuse_port = true,
.nonblocking = true,
});
defer srv.deinit();
srv.listen(mode.addr) catch |err| {
log.err("address (host:port) {any}\n", .{err});
return printUsageExit(mode.execname, 1);
};
defer srv.close();
log.info("Server mode: listening on {s}:{d}...", .{ mode.host, mode.port });
// loop
var loop = try jsruntime.Loop.init(alloc);
defer loop.deinit();
// listen
try server.listen(alloc, &loop, srv.sockfd.?, std.time.ns_per_s * @as(u64, mode.timeout));
},
.fetch => |mode| {
log.debug("Fetch mode: url {s}, dump {any}", .{ mode.url, mode.dump });
// vm
const vm = jsruntime.VM.init();
defer vm.deinit();
// loop
var loop = try jsruntime.Loop.init(alloc);
defer loop.deinit();
// browser
var browser = Browser{};
try Browser.init(&browser, alloc, &loop, vm);
defer browser.deinit();
// page
const page = try browser.session.createPage();
_ = page.navigate(mode.url, null) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => {
log.err("'{s}' is not a valid URL ({any})\n", .{ mode.url, err });
return printUsageExit(mode.execname, 1);
},
else => {
log.err("'{s}' fetching error ({any})s\n", .{ mode.url, err });
return printUsageExit(mode.execname, 1);
},
};
try page.wait();
// dump
if (mode.dump) {
try page.dump(std.io.getStdOut());
}
},
}
}

View File

@@ -1,97 +0,0 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Browser = @import("browser/browser.zig").Browser;
const jsruntime = @import("jsruntime");
const apiweb = @import("apiweb.zig");
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
pub const std_options = std.Options{
.log_level = .debug,
};
const usage =
\\usage: {s} [options] <url>
\\ request the url with the browser
\\
\\ -h, --help Print this help message and exit.
\\ --dump Dump document in stdout
\\
;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const check = gpa.deinit();
if (check == .leak) {
std.log.warn("leaks detected\n", .{});
}
}
const allocator = gpa.allocator();
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
const execname = args.next().?;
var url: []const u8 = "";
var dump: bool = false;
while (args.next()) |arg| {
if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.posix.exit(0);
}
if (std.mem.eql(u8, "--dump", arg)) {
dump = true;
continue;
}
// allow only one url
if (url.len != 0) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.posix.exit(1);
}
url = arg;
}
if (url.len == 0) {
try std.io.getStdErr().writer().print(usage, .{execname});
std.posix.exit(1);
}
const vm = jsruntime.VM.init();
defer vm.deinit();
var browser = try Browser.init(allocator, vm);
defer browser.deinit();
var page = try browser.currentSession().createPage();
defer page.deinit();
try page.navigate(url);
defer page.end();
try page.wait();
if (dump) {
try page.dump(std.io.getStdOut());
}
}

View File

@@ -24,12 +24,13 @@ const parser = @import("netsurf");
const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window;
const storage = @import("storage/storage.zig");
const Client = @import("asyncio").Client;
const html_test = @import("html_test.zig").html;
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = apiweb.UserContext;
const Client = @import("async/Client.zig");
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
var doc: *parser.DocumentHTML = undefined;
@@ -41,7 +42,7 @@ fn execJS(
try js_env.start();
defer js_env.stop();
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
var cli = Client{ .allocator = alloc };
defer cli.deinit();
try js_env.setUserContext(UserContext{
@@ -87,5 +88,5 @@ pub fn main() !void {
defer vm.deinit();
// launch shell
try jsruntime.shell(&arena, execJS, .{ .app_name = "browsercore" });
try jsruntime.shell(&arena, execJS, .{ .app_name = "lightpanda-shell" });
}

View File

@@ -50,11 +50,12 @@ const Out = enum {
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const GlobalType = apiweb.GlobalType;
pub const UserContext = apiweb.UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
// TODO For now the WPT tests run is specific to WPT.
// It manually load js framwork libs, and run the first script w/ js content in
// the HTML page.
// Once browsercore will have the html loader, it would be useful to refacto
// Once lightpanda will have the html loader, it would be useful to refacto
// this test to use it.
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};

172
src/msg.zig Normal file
View File

@@ -0,0 +1,172 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
/// MsgBuffer returns messages from a raw text read stream,
/// according to the following format `<msg_size>:<msg>`.
/// It handles both:
/// - combined messages in one read
/// - single message in several reads (multipart)
/// It's safe (and a good practice) to reuse the same MsgBuffer
/// on several reads of the same stream.
pub const MsgBuffer = struct {
size: usize = 0,
buf: []u8,
pos: usize = 0,
const MaxSize = 1024 * 1024; // 1MB
pub fn init(alloc: std.mem.Allocator, size: usize) std.mem.Allocator.Error!MsgBuffer {
const buf = try alloc.alloc(u8, size);
return .{ .buf = buf };
}
pub fn deinit(self: MsgBuffer, alloc: std.mem.Allocator) void {
alloc.free(self.buf);
}
fn isFinished(self: *MsgBuffer) bool {
return self.pos >= self.size;
}
fn isEmpty(self: MsgBuffer) bool {
return self.size == 0 and self.pos == 0;
}
fn reset(self: *MsgBuffer) void {
self.size = 0;
self.pos = 0;
}
// read input
pub fn read(self: *MsgBuffer, alloc: std.mem.Allocator, input: []const u8) !struct {
msg: []const u8,
left: []const u8,
} {
var _input = input; // make input writable
// msg size
var msg_size: usize = undefined;
if (self.isEmpty()) {
// parse msg size metadata
const size_pos = std.mem.indexOfScalar(u8, _input, ':') orelse return error.InputWithoutSize;
const size_str = _input[0..size_pos];
msg_size = try std.fmt.parseInt(u32, size_str, 10);
_input = _input[size_pos + 1 ..];
} else {
msg_size = self.size;
}
// multipart
const is_multipart = !self.isEmpty() or _input.len < msg_size;
if (is_multipart) {
// set msg size on empty MsgBuffer
if (self.isEmpty()) {
self.size = msg_size;
}
// get the new position of the cursor
const new_pos = self.pos + _input.len;
// check max limit size
if (new_pos > MaxSize) {
return error.MsgTooBig;
}
// check if the current input can fit in MsgBuffer
if (new_pos > self.buf.len) {
// we want to realloc at least:
// - a size big enough to fit the entire input (ie. new_pos)
// - a size big enough (ie. current msg size + starting buffer size)
// to avoid multiple reallocation
const new_size = @max(self.buf.len + self.size, new_pos);
// resize the MsgBuffer to fit
self.buf = try alloc.realloc(self.buf, new_size);
}
// copy the current input into MsgBuffer
@memcpy(self.buf[self.pos..new_pos], _input[0..]);
// set the new cursor position
self.pos = new_pos;
// if multipart is not finished, go fetch the next input
if (!self.isFinished()) return error.MsgMultipart;
// otherwhise multipart is finished, use its buffer as input
_input = self.buf[0..self.pos];
self.reset();
}
// handle several JSON msg in 1 read
return .{ .msg = _input[0..msg_size], .left = _input[msg_size..] };
}
};
fn doTest(nb: *u8) void {
nb.* += 1;
}
test "MsgBuffer" {
const Case = struct {
input: []const u8,
nb: u8,
};
const alloc = std.testing.allocator;
const cases = [_]Case{
// simple
.{ .input = "2:ok", .nb = 1 },
// combined
.{ .input = "2:ok3:foo7:bar2:ok", .nb = 3 }, // "bar2:ok" is a message, no need to escape "2:" here
// multipart
.{ .input = "9:multi", .nb = 0 },
.{ .input = "part", .nb = 1 },
// multipart & combined
.{ .input = "9:multi", .nb = 0 },
.{ .input = "part2:ok", .nb = 2 },
// multipart & combined with other multipart
.{ .input = "9:multi", .nb = 0 },
.{ .input = "part8:co", .nb = 1 },
.{ .input = "mbined", .nb = 1 },
// several multipart
.{ .input = "23:multi", .nb = 0 },
.{ .input = "several", .nb = 0 },
.{ .input = "complex", .nb = 0 },
.{ .input = "part", .nb = 1 },
// combined & multipart
.{ .input = "2:ok9:multi", .nb = 1 },
.{ .input = "part", .nb = 1 },
};
var msg_buf = try MsgBuffer.init(alloc, 10);
defer msg_buf.deinit(alloc);
for (cases) |case| {
var nb: u8 = 0;
var input: []const u8 = case.input;
while (input.len > 0) {
const parts = msg_buf.read(alloc, input) catch |err| {
if (err == error.MsgMultipart) break; // go to the next case input
return err;
};
nb += 1;
input = parts.left;
}
try std.testing.expect(nb == case.nb);
}
}

View File

@@ -1794,7 +1794,7 @@ pub fn documentFragmentBodyChildren(doc: *DocumentFragment) !?*NodeList {
// Document Position
pub const DocumentPosition = enum(u2) {
pub const DocumentPosition = enum(u32) {
disconnected = c.DOM_DOCUMENT_POSITION_DISCONNECTED,
preceding = c.DOM_DOCUMENT_POSITION_PRECEDING,
following = c.DOM_DOCUMENT_POSITION_FOLLOWING,
@@ -2249,3 +2249,18 @@ pub inline fn documentHTMLSetTitle(doc: *DocumentHTML, v: []const u8) !void {
const err = documentHTMLVtable(doc).set_title.?(doc, try strFromData(v));
try DOMErr(err);
}
pub fn documentHTMLSetCurrentScript(doc: *DocumentHTML, script: ?*Script) !void {
var s: ?*ElementHTML = null;
if (script != null) s = @ptrCast(script.?);
const err = documentHTMLVtable(doc).set_current_script.?(doc, s);
try DOMErr(err);
}
pub fn documentHTMLGetCurrentScript(doc: *DocumentHTML) !?*Script {
var elem: ?*ElementHTML = undefined;
const err = documentHTMLVtable(doc).get_current_script.?(doc, &elem);
try DOMErr(err);
if (elem == null) return null;
return @ptrCast(elem.?);
}

View File

@@ -30,7 +30,7 @@ const xhr = @import("xhr/xhr.zig");
const storage = @import("storage/storage.zig");
const url = @import("url/url.zig");
const urlquery = @import("url/query.zig");
const Client = @import("async/Client.zig");
const Client = @import("asyncio").Client;
const documentTestExecFn = @import("dom/document.zig").testExecFn;
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
@@ -59,6 +59,7 @@ const MutationObserverTestExecFn = @import("dom/mutation_observer.zig").testExec
pub const Types = jsruntime.reflect(apiweb.Interfaces);
pub const UserContext = @import("user_context.zig").UserContext;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
var doc: *parser.DocumentHTML = undefined;
@@ -86,7 +87,7 @@ fn testExecFn(
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
};
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
var cli = Client{ .allocator = alloc };
defer cli.deinit();
try js_env.setUserContext(.{
@@ -283,7 +284,7 @@ fn run_js(out: Out) !void {
const row_shape = .{ []const u8, pretty.Measure, u64, u64, pretty.Measure };
const table = try pretty.GenerateTable(4, row_shape, pretty.TableConf{ .margin_left = " " });
const header = .{ "FUNCTION", "DURATION", "ALLOCATIONS (nb)", "RE-ALLOCATIONS (nb)", "HEAP SIZE" };
var t = table.init("Benchmark browsercore 🚀", header);
var t = table.init("Benchmark lightpanda 🚀", header);
try t.addRow(.{ "browser", dur, stats.alloc_nb, stats.realloc_nb, size });
try t.addRow(.{ "libdom", dur, 0, 0, zerosize }); // TODO get libdom bench info.
try t.addRow(.{ "v8", dur, 0, 0, zerosize }); // TODO get v8 bench info.
@@ -295,8 +296,8 @@ const kb = 1024;
const ms = std.time.ns_per_ms;
test {
const asyncTest = @import("async/test.zig");
std.testing.refAllDecls(asyncTest);
const msgTest = @import("msg.zig");
std.testing.refAllDecls(msgTest);
const dumpTest = @import("browser/dump.zig");
std.testing.refAllDecls(dumpTest);
@@ -318,6 +319,8 @@ test {
const queryTest = @import("url/query.zig");
std.testing.refAllDecls(queryTest);
std.testing.refAllDecls(@import("cdp/msg.zig"));
}
fn testJSRuntime(alloc: std.mem.Allocator) !void {

499
src/server.zig Normal file
View File

@@ -0,0 +1,499 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const Completion = jsruntime.IO.Completion;
const AcceptError = jsruntime.IO.AcceptError;
const RecvError = jsruntime.IO.RecvError;
const SendError = jsruntime.IO.SendError;
const CloseError = jsruntime.IO.CloseError;
const CancelError = jsruntime.IO.CancelError;
const TimeoutError = jsruntime.IO.TimeoutError;
const MsgBuffer = @import("msg.zig").MsgBuffer;
const Browser = @import("browser/browser.zig").Browser;
const cdp = @import("cdp/cdp.zig");
const NoError = error{NoError};
const IOError = AcceptError || RecvError || SendError || CloseError || TimeoutError || CancelError;
const Error = IOError || std.fmt.ParseIntError || cdp.Error || NoError;
const TimeoutCheck = std.time.ns_per_ms * 100;
const log = std.log.scoped(.server);
const isLinux = builtin.target.os.tag == .linux;
// I/O Main
// --------
const BufReadSize = 1024; // 1KB
const MaxStdOutSize = 512; // ensure debug msg are not too long
pub const Ctx = struct {
loop: *jsruntime.Loop,
// internal fields
accept_socket: std.posix.socket_t,
conn_socket: std.posix.socket_t = undefined,
read_buf: []u8, // only for read operations
msg_buf: *MsgBuffer,
err: ?Error = null,
// I/O fields
accept_completion: *Completion,
conn_completion: *Completion,
timeout_completion: *Completion,
timeout: u64,
last_active: ?std.time.Instant = null,
// CDP
state: cdp.State = .{},
// JS fields
browser: *Browser, // TODO: is pointer mandatory here?
sessionNew: bool,
// try_catch: jsruntime.TryCatch, // TODO
// callbacks
// ---------
fn acceptCbk(
self: *Ctx,
completion: *Completion,
result: AcceptError!std.posix.socket_t,
) void {
std.debug.assert(completion == self.acceptCompletion());
self.conn_socket = result catch |err| {
log.err("accept error: {any}", .{err});
self.err = err;
return;
};
log.info("client connected", .{});
// set connection timestamp and timeout
self.last_active = std.time.Instant.now() catch |err| {
log.err("accept timestamp error: {any}", .{err});
return;
};
self.loop.io.timeout(
*Ctx,
self,
Ctx.timeoutCbk,
self.timeout_completion,
TimeoutCheck,
);
// receving incomming messages asynchronously
self.loop.io.recv(
*Ctx,
self,
Ctx.readCbk,
self.conn_completion,
self.conn_socket,
self.read_buf,
);
}
fn readCbk(self: *Ctx, completion: *Completion, result: RecvError!usize) void {
std.debug.assert(completion == self.conn_completion);
const size = result catch |err| {
if (err == error.Canceled) {
log.debug("read canceled", .{});
return;
}
log.err("read error: {any}", .{err});
self.err = err;
return;
};
if (size == 0) {
// continue receving incomming messages asynchronously
self.loop.io.recv(
*Ctx,
self,
Ctx.readCbk,
self.conn_completion,
self.conn_socket,
self.read_buf,
);
return;
}
// set connection timestamp
self.last_active = std.time.Instant.now() catch |err| {
log.err("read timestamp error: {any}", .{err});
return;
};
// continue receving incomming messages asynchronously
self.loop.io.recv(
*Ctx,
self,
Ctx.readCbk,
self.conn_completion,
self.conn_socket,
self.read_buf,
);
// read and execute input
var input: []const u8 = self.read_buf[0..size];
while (input.len > 0) {
const parts = self.msg_buf.read(self.alloc(), input) catch |err| {
if (err == error.MsgMultipart) {
return;
} else {
log.err("msg read error: {any}", .{err});
return;
}
};
input = parts.left;
// execute
self.do(parts.msg) catch |err| {
if (err != error.Closed) {
log.err("do error: {any}", .{err});
}
};
}
}
fn timeoutCbk(self: *Ctx, completion: *Completion, result: TimeoutError!void) void {
std.debug.assert(completion == self.timeout_completion);
_ = result catch |err| {
log.err("timeout error: {any}", .{err});
self.err = err;
return;
};
if (self.isClosed()) {
// conn is already closed, ignore timeout
return;
}
// check time since last read
const now = std.time.Instant.now() catch |err| {
log.err("timeout timestamp error: {any}", .{err});
return;
};
if (now.since(self.last_active.?) > self.timeout) {
// close current connection
log.debug("conn timeout, closing...", .{});
self.cancelAndClose();
return;
}
// continue checking timeout
self.loop.io.timeout(
*Ctx,
self,
Ctx.timeoutCbk,
self.timeout_completion,
TimeoutCheck,
);
}
fn cancelCbk(self: *Ctx, completion: *Completion, result: CancelError!void) void {
std.debug.assert(completion == self.accept_completion);
_ = result catch |err| {
log.err("cancel error: {any}", .{err});
self.err = err;
return;
};
log.debug("cancel done", .{});
self.close();
}
// shortcuts
// ---------
inline fn isClosed(self: *Ctx) bool {
// last_active is first saved on acceptCbk
return self.last_active == null;
}
// allocator of the current session
inline fn alloc(self: *Ctx) std.mem.Allocator {
return self.browser.session.alloc;
}
// JS env of the current session
inline fn env(self: Ctx) jsruntime.Env {
return self.browser.session.env;
}
inline fn acceptCompletion(self: *Ctx) *Completion {
// NOTE: the logical completion to use here is the accept_completion
// as the pipe_connection can be used simulteanously by a recv I/O operation.
// But on MacOS (kqueue) the recv I/O operation on a closed socket leads to a panic
// so we use the pipe_connection to avoid this problem
if (isLinux) return self.accept_completion;
return self.conn_completion;
}
// actions
// -------
fn do(self: *Ctx, cmd: []const u8) anyerror!void {
// close cmd
if (std.mem.eql(u8, cmd, "close")) {
// close connection
log.info("close cmd, closing conn...", .{});
self.cancelAndClose();
return error.Closed;
}
if (self.sessionNew) self.sessionNew = false;
const res = cdp.do(self.alloc(), cmd, self) catch |err| {
// cdp end cmd
if (err == error.DisposeBrowserContext) {
// restart a new browser session
std.log.scoped(.cdp).debug("end cmd, restarting a new session...", .{});
try self.newSession();
return;
}
return err;
};
// send result
if (!std.mem.eql(u8, res, "")) {
return sendAsync(self, res);
}
}
fn cancelAndClose(self: *Ctx) void {
if (isLinux) { // cancel is only available on Linux
self.loop.io.cancel(
*Ctx,
self,
Ctx.cancelCbk,
self.accept_completion,
self.conn_completion,
);
} else {
self.close();
}
}
fn close(self: *Ctx) void {
std.posix.close(self.conn_socket);
// conn is closed
log.debug("connection closed", .{});
self.last_active = null;
// restart a new browser session in case of re-connect
if (!self.sessionNew) {
self.newSession() catch |err| {
log.err("new session error: {any}", .{err});
return;
};
}
log.info("accepting new conn...", .{});
// continue accepting incoming requests
self.loop.io.accept(
*Ctx,
self,
Ctx.acceptCbk,
self.acceptCompletion(),
self.accept_socket,
);
}
fn newSession(self: *Ctx) !void {
try self.browser.newSession(self.alloc(), self.loop);
try self.browser.session.initInspector(
self,
Ctx.onInspectorResp,
Ctx.onInspectorNotif,
);
self.sessionNew = true;
}
// inspector
// ---------
pub fn sendInspector(self: *Ctx, msg: []const u8) void {
if (self.env().getInspector()) |inspector| {
inspector.send(self.env(), msg);
} else @panic("Inspector has not been set");
}
inline fn inspectorCtx(ctx_opaque: *anyopaque) *Ctx {
const aligned = @as(*align(@alignOf(Ctx)) anyopaque, @alignCast(ctx_opaque));
return @as(*Ctx, @ptrCast(aligned));
}
fn inspectorMsg(allocator: std.mem.Allocator, ctx: *Ctx, msg: []const u8) !void {
// inject sessionID in cdp msg
const tpl = "{s},\"sessionId\":\"{s}\"}}";
const msg_open = msg[0 .. msg.len - 1]; // remove closing bracket
const s = try std.fmt.allocPrint(
allocator,
tpl,
.{ msg_open, cdp.ContextSessionID },
);
try sendAsync(ctx, s);
}
pub fn onInspectorResp(ctx_opaque: *anyopaque, _: u32, msg: []const u8) void {
if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"id":<id>,...
const id_end = std.mem.indexOfScalar(u8, msg, ',') orelse unreachable;
const id = msg[6..id_end];
std.log.scoped(.cdp).debug("Res (inspector) > id {s}", .{id});
}
const ctx = inspectorCtx(ctx_opaque);
inspectorMsg(ctx.alloc(), ctx, msg) catch unreachable;
}
pub fn onInspectorNotif(ctx_opaque: *anyopaque, msg: []const u8) void {
if (std.log.defaultLogEnabled(.debug)) {
// msg should be {"method":<method>,...
const method_end = std.mem.indexOfScalar(u8, msg, ',') orelse unreachable;
const method = msg[10..method_end];
std.log.scoped(.cdp).debug("Event (inspector) > method {s}", .{method});
}
const ctx = inspectorCtx(ctx_opaque);
inspectorMsg(ctx.alloc(), ctx, msg) catch unreachable;
}
};
// I/O Send
// --------
// NOTE: to allow concurrent send we create each time a dedicated context
// (with its own completion), allocated on the heap.
// After the send (on the sendCbk) the dedicated context will be destroy
// and the msg slice will be free.
const Send = struct {
ctx: *Ctx,
msg: []const u8,
completion: Completion = undefined,
fn init(ctx: *Ctx, msg: []const u8) !*Send {
const sd = try ctx.alloc().create(Send);
sd.* = .{ .ctx = ctx, .msg = msg };
return sd;
}
fn deinit(self: *Send) void {
self.ctx.alloc().free(self.msg);
self.ctx.alloc().destroy(self);
}
fn asyncCbk(self: *Send, _: *Completion, result: SendError!usize) void {
_ = result catch |err| {
log.err("send error: {any}", .{err});
self.ctx.err = err;
};
self.deinit();
}
};
pub fn sendAsync(ctx: *Ctx, msg: []const u8) !void {
const sd = try Send.init(ctx, msg);
ctx.loop.io.send(*Send, sd, Send.asyncCbk, &sd.completion, ctx.conn_socket, msg);
}
// Listen
// ------
pub fn listen(
alloc: std.mem.Allocator,
loop: *jsruntime.Loop,
server_socket: std.posix.socket_t,
timeout: u64,
) anyerror!void {
// create v8 vm
const vm = jsruntime.VM.init();
defer vm.deinit();
// browser
var browser: Browser = undefined;
try Browser.init(&browser, alloc, loop, vm);
defer browser.deinit();
// create buffers
var read_buf: [BufReadSize]u8 = undefined;
var msg_buf = try MsgBuffer.init(loop.alloc, BufReadSize * 256); // 256KB
defer msg_buf.deinit(loop.alloc);
// create I/O completions
var accept_completion: Completion = undefined;
var conn_completion: Completion = undefined;
var timeout_completion: Completion = undefined;
// create I/O contexts and callbacks
// for accepting connections and receving messages
var ctx = Ctx{
.loop = loop,
.browser = &browser,
.sessionNew = true,
.read_buf = &read_buf,
.msg_buf = &msg_buf,
.accept_socket = server_socket,
.timeout = timeout,
.accept_completion = &accept_completion,
.conn_completion = &conn_completion,
.timeout_completion = &timeout_completion,
};
try browser.session.initInspector(
&ctx,
Ctx.onInspectorResp,
Ctx.onInspectorNotif,
);
// accepting connection asynchronously on internal server
log.info("accepting new conn...", .{});
loop.io.accept(*Ctx, &ctx, Ctx.acceptCbk, ctx.acceptCompletion(), ctx.accept_socket);
// infinite loop on I/O events, either:
// - cmd from incoming connection on server socket
// - JS callbacks events from scripts
while (true) {
try loop.io.run_for_ns(10 * std.time.ns_per_ms);
if (loop.cbk_error) {
log.err("JS error", .{});
// if (try try_catch.exception(alloc, js_env.*)) |msg| {
// std.debug.print("\n\rUncaught {s}\n\r", .{msg});
// alloc.free(msg);
// }
// loop.cbk_error = false;
}
if (ctx.err) |err| {
if (err != error.NoError) log.err("Server error: {any}", .{err});
break;
}
}
}

View File

@@ -22,6 +22,7 @@ const tests = @import("run_tests.zig");
pub const Types = tests.Types;
pub const UserContext = tests.UserContext;
pub const IO = tests.IO;
pub fn main() !void {
try tests.main();

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const parser = @import("netsurf");
const Client = @import("async/Client.zig");
const Client = @import("asyncio").Client;
pub const UserContext = struct {
document: *parser.DocumentHTML,

View File

@@ -28,10 +28,10 @@ const Loop = jsruntime.Loop;
const Env = jsruntime.Env;
const Window = @import("../html/window.zig").Window;
const storage = @import("../storage/storage.zig");
const Client = @import("asyncio").Client;
const Types = @import("../main_wpt.zig").Types;
const UserContext = @import("../main_wpt.zig").UserContext;
const Client = @import("../async/Client.zig");
// runWPT parses the given HTML file, starts a js env and run the first script
// tags containing javascript sources.
@@ -53,10 +53,11 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
var loop = try Loop.init(alloc);
defer loop.deinit();
var cli = Client{ .allocator = alloc, .loop = &loop };
var cli = Client{ .allocator = alloc };
defer cli.deinit();
var js_env = try Env.init(alloc, &loop, UserContext{
var js_env: Env = undefined;
Env.init(&js_env, alloc, &loop, UserContext{
.document = html_doc,
.httpClient = &cli,
});

View File

@@ -32,8 +32,7 @@ const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEven
const Mime = @import("../browser/mime.zig");
const Loop = jsruntime.Loop;
const YieldImpl = Loop.Yield(XMLHttpRequest);
const Client = @import("../async/Client.zig");
const Client = @import("asyncio").Client;
const parser = @import("netsurf");
@@ -98,10 +97,11 @@ pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator,
cli: *Client,
impl: YieldImpl,
io: Client.IO,
priv_state: PrivState = .new,
req: ?Client.Request = null,
ctx: ?Client.Ctx = null,
method: std.http.Method,
state: u16,
@@ -135,7 +135,13 @@ pub const XMLHttpRequest = struct {
response_header_buffer: [1024 * 16]u8 = undefined,
response_status: u10 = 0,
response_override_mime_type: ?[]const u8 = null,
// TODO uncomment this field causes casting issue with
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
// not sure. see
// https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019
// response_override_mime_type: ?[]const u8 = null,
response_mime: Mime = undefined,
response_obj: ?ResponseObj = null,
send_flag: bool = false,
@@ -288,7 +294,7 @@ pub const XMLHttpRequest = struct {
.alloc = alloc,
.headers = Headers.init(alloc),
.response_headers = Headers.init(alloc),
.impl = YieldImpl.init(loop),
.io = Client.IO.init(loop),
.method = undefined,
.url = null,
.uri = undefined,
@@ -320,10 +326,11 @@ pub const XMLHttpRequest = struct {
self.priv_state = .new;
if (self.req) |*r| {
r.deinit();
self.req = null;
}
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
@@ -382,7 +389,11 @@ pub const XMLHttpRequest = struct {
self.reset(alloc);
self.url = try alloc.dupe(u8, url);
self.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax;
self.uri = std.Uri.parse(self.url.?) catch |err| {
log.debug("parse url ({s}): {any}", .{ self.url.?, err });
return DOMError.Syntax;
};
log.debug("open url ({s})", .{self.url.?});
self.sync = if (asyn) |b| !b else false;
self.state = OPENED;
@@ -494,138 +505,160 @@ pub const XMLHttpRequest = struct {
log.debug("{any} {any}", .{ self.method, self.uri });
self.send_flag = true;
self.impl.yield(self);
}
// onYield is a callback called between each request's steps.
// Between each step, the code is blocking.
// Yielding allows pseudo-async and gives a chance to other async process
// to be called.
pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void {
if (err) |e| return self.onErr(e);
self.priv_state = .open;
switch (self.priv_state) {
.new => {
self.priv_state = .open;
self.req = self.cli.open(self.method, self.uri, .{
.server_header_buffer = &self.response_header_buffer,
.extra_headers = self.headers.all(),
}) catch |e| return self.onErr(e);
},
.open => {
// prepare payload transfert.
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
self.priv_state = .send;
self.req.?.send() catch |e| return self.onErr(e);
},
.send => {
if (self.payload) |payload| {
self.priv_state = .write;
self.req.?.writeAll(payload) catch |e| return self.onErr(e);
} else {
self.priv_state = .finish;
self.req.?.finish() catch |e| return self.onErr(e);
}
},
.write => {
self.priv_state = .finish;
self.req.?.finish() catch |e| return self.onErr(e);
},
.finish => {
self.priv_state = .wait;
self.req.?.wait() catch |e| return self.onErr(e);
},
.wait => {
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
self.priv_state = .done;
var it = self.req.?.response.iterateHeaders();
self.response_headers.load(&it) catch |e| return self.onErr(e);
// extract a mime type from headers.
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
self.response_mime = Mime.parse(ct) catch |e| return self.onErr(e);
// TODO handle override mime type
self.state = HEADERS_RECEIVED;
self.dispatchEvt("readystatechange");
self.response_status = @intFromEnum(self.req.?.response.status);
var buf: std.ArrayListUnmanaged(u8) = .{};
// TODO set correct length
const total = 0;
var loaded: u64 = 0;
// dispatch a progress event loadstart.
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
const reader = self.req.?.reader();
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
var prev_dispatch: ?std.time.Instant = null;
while (ln > 0) {
ln = reader.read(&buffer) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
loaded = loaded + ln;
// Dispatch only if 50ms have passed.
const now = std.time.Instant.now() catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
defer prev_dispatch = now;
self.state = LOADING;
self.dispatchEvt("readystatechange");
// dispatch a progress event progress.
self.dispatchProgressEvent("progress", .{
.loaded = loaded,
.total = total,
});
}
self.response_bytes = buf.items;
self.send_flag = false;
self.state = DONE;
self.dispatchEvt("readystatechange");
// dispatch a progress event load.
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
// dispatch a progress event loadend.
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
},
.done => {
if (self.req) |*r| {
r.deinit();
self.req = null;
}
// finalize fetch process.
return;
},
self.req = try self.cli.create(self.method, self.uri, .{
.server_header_buffer = &self.response_header_buffer,
.extra_headers = self.headers.all(),
});
errdefer {
self.req.?.deinit();
self.req = null;
}
self.impl.yield(self);
self.ctx = try Client.Ctx.init(&self.io, &self.req.?);
errdefer {
self.ctx.?.deinit();
self.ctx = null;
}
self.ctx.?.userData = self;
try self.cli.async_open(
self.method,
self.uri,
.{ .server_header_buffer = &self.response_header_buffer },
&self.ctx.?,
onRequestConnect,
);
}
fn onRequestWait(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
self.priv_state = .done;
var it = self.req.?.response.iterateHeaders();
self.response_headers.load(&it) catch |e| return self.onErr(e);
// extract a mime type from headers.
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
self.response_mime = Mime.parse(ct) catch |e| return self.onErr(e);
// TODO handle override mime type
self.state = HEADERS_RECEIVED;
self.dispatchEvt("readystatechange");
self.response_status = @intFromEnum(self.req.?.response.status);
var buf: std.ArrayListUnmanaged(u8) = .{};
// TODO set correct length
const total = 0;
var loaded: u64 = 0;
// dispatch a progress event loadstart.
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
// TODO read async
const reader = self.req.?.reader();
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
var prev_dispatch: ?std.time.Instant = null;
while (ln > 0) {
ln = reader.read(&buffer) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
loaded = loaded + ln;
// Dispatch only if 50ms have passed.
const now = std.time.Instant.now() catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
defer prev_dispatch = now;
self.state = LOADING;
self.dispatchEvt("readystatechange");
// dispatch a progress event progress.
self.dispatchProgressEvent("progress", .{
.loaded = loaded,
.total = total,
});
}
self.response_bytes = buf.items;
self.send_flag = false;
self.state = DONE;
self.dispatchEvt("readystatechange");
// dispatch a progress event load.
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
// dispatch a progress event loadend.
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
fn onRequestFinish(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
self.priv_state = .wait;
return ctx.req.async_wait(ctx, onRequestWait) catch |e| return self.onErr(e);
}
fn onRequestSend(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
if (self.payload) |payload| {
self.priv_state = .write;
return ctx.req.async_writeAll(payload, ctx, onRequestWrite) catch |e| return self.onErr(e);
}
self.priv_state = .finish;
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
}
fn onRequestWrite(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
self.priv_state = .finish;
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
}
fn onRequestConnect(ctx: *Client.Ctx, res: anyerror!void) anyerror!void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
// prepare payload transfert.
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
self.priv_state = .send;
return ctx.req.async_send(ctx, onRequestSend) catch |err| return self.onErr(err);
}
fn selfCtx(ctx: *Client.Ctx) *XMLHttpRequest {
return @ptrCast(@alignCast(ctx.userData));
}
fn onErr(self: *XMLHttpRequest, err: anyerror) void {
self.priv_state = .done;
if (self.req) |*r| {
r.deinit();
self.req = null;
}
self.err = err;
self.state = DONE;
@@ -635,6 +668,12 @@ pub const XMLHttpRequest = struct {
self.dispatchProgressEvent("loadend", .{});
log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err });
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
pub fn _abort(self: *XMLHttpRequest) void {
@@ -882,7 +921,7 @@ pub fn testExecFn(
// .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" },
//.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" },
.{ .src = "req.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
// ensure open resets values
@@ -912,7 +951,7 @@ pub fn testExecFn(
var document = [_]Case{
.{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req2.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req2.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req2.responseType = 'document'", .ex = "document" },
.{ .src = "req2.send()", .ex = "undefined" },
@@ -928,7 +967,7 @@ pub fn testExecFn(
var json = [_]Case{
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req3.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req3.responseType = 'json'", .ex = "json" },
.{ .src = "req3.send()", .ex = "undefined" },
@@ -943,7 +982,7 @@ pub fn testExecFn(
var post = [_]Case{
.{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req4.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req4.open('POST', 'https://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req4.send('foo')", .ex = "undefined" },
// Each case executed waits for all loop callaback calls.
@@ -956,7 +995,7 @@ pub fn testExecFn(
var cbk = [_]Case{
.{ .src = "const req5 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req5.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req5.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
.{ .src = "var status = 0; req5.onload = function () { status = this.status };", .ex = "function () { status = this.status }" },
.{ .src = "req5.send()", .ex = "undefined" },

1
vendor/zig-async-io vendored Submodule

Submodule vendor/zig-async-io added at ed7ae07d1c