1 Commits

55 changed files with 2695 additions and 3932 deletions

View File

@@ -17,7 +17,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.9'
default: 'v0.1.6'
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 }}_${{ inputs.zig-v8 }}.a
key: libc_v8_${{ inputs.v8 }}_${{ inputs.os }}_${{ inputs.arch }}.a
- if: ${{ steps.cache-v8.outputs.cache-hit != 'true' }}
shell: bash

View File

@@ -16,12 +16,13 @@ jobs:
ARCH: x86_64
OS: linux
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
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
@@ -31,13 +32,13 @@ jobs:
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8 -Dcpu=x86_64
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
tag: nightly
build-macos-aarch64:
@@ -51,6 +52,7 @@ 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
@@ -63,11 +65,11 @@ jobs:
run: zig build --release=safe -Doptimize=ReleaseSafe -Dengine=v8
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
run: mv zig-out/bin/browsercore-get lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
- name: Upload the build
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
artifacts: lightpanda-get-${{ env.ARCH }}-${{ env.OS }}
tag: nightly

View File

@@ -1,32 +0,0 @@
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,6 +50,7 @@ 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,6 +48,7 @@ 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
@@ -68,6 +69,7 @@ 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
@@ -88,6 +90,7 @@ 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,5 +1,7 @@
zig-cache
/.zig-cache/
zig-out
/vendor/netsurf/out
/vendor/netsurf/build/
/vendor/netsurf/lib/
/vendor/netsurf/include/
/vendor/libiconv/

3
.gitmodules vendored
View File

@@ -25,6 +25,3 @@
[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
View File

@@ -1,93 +0,0 @@
# 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]”.

View File

@@ -1,77 +0,0 @@
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"]

View File

@@ -1,22 +0,0 @@
# 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,25 +4,6 @@
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
@@ -45,11 +26,30 @@ 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:
$(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")
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")
@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/lightpanda || (printf "\e[33mRun ERROR\e[0m\n"; exit 1;)
@./zig-out/bin/browsercore || (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-libiconv install-netsurf install-mimalloc
install: install-submodule install-zig-js-runtime install-netsurf install-mimalloc
## Install and build dependencies for dev
install-dev: install-submodule install-zig-js-runtime-dev install-libiconv install-netsurf-dev install-mimalloc-dev
install-dev: install-submodule install-zig-js-runtime-dev install-netsurf-dev install-mimalloc-dev
install-netsurf-dev: _install-netsurf
install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
@@ -111,16 +111,14 @@ install-netsurf-dev: OPTCFLAGS := -O0 -g -DNDEBUG
install-netsurf: _install-netsurf
install-netsurf: OPTCFLAGS := -DNDEBUG
BC_NS := $(BC)vendor/netsurf/out/$(OS)-$(ARCH)
ICONV := $(BC)vendor/libiconv/out/$(OS)-$(ARCH)
BC_NS := $(BC)vendor/netsurf
ICONV := $(BC)vendor/libiconv
# 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: clean-netsurf
_install-netsurf: install-libiconv
@printf "\e[36mInstalling NetSurf...\e[0m\n" && \
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) && \
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;) && \
export PREFIX=$(BC_NS) && \
export OPTLDFLAGS="-L$(ICONV)/lib" && \
export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \
@@ -158,7 +156,10 @@ _install-netsurf: clean-netsurf
clean-netsurf:
@printf "\e[36mCleaning NetSurf build...\e[0m\n" && \
rm -Rf $(BC_NS)
cd vendor/netsurf && \
rm -R build && \
rm -R lib && \
rm -R include
test-netsurf:
@printf "\e[36mTesting NetSurf...\e[0m\n" && \
@@ -168,22 +169,14 @@ test-netsurf:
cd vendor/netsurf/libdom && \
BUILDDIR=$(BC_NS)/build/libdom make test
download-libiconv:
ifeq ("$(wildcard vendor/libiconv/libiconv-1.17)","")
install-libiconv:
ifeq ("$(wildcard vendor/libiconv/lib/libiconv.a)","")
@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=$(ICONV) --enable-static && \
./configure --prefix=$(BC)vendor/libiconv --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:
@@ -195,28 +188,24 @@ install-zig-js-runtime:
make install
.PHONY: _build_mimalloc
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
_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
install-mimalloc-dev: _build_mimalloc
install-mimalloc-dev: OPTS=-DCMAKE_BUILD_TYPE=Debug
install-mimalloc-dev:
@cd $(MIMALLOC) && \
mv build/libmimalloc-debug.a lib/libmimalloc.a
@cd vendor/mimalloc/out && \
mv libmimalloc-debug.a libmimalloc.a
install-mimalloc: _build_mimalloc
install-mimalloc:
@cd $(MIMALLOC) && \
mv build/libmimalloc.a lib/libmimalloc.a
clean-mimalloc:
@rm -Rf $(MIMALLOC)/build
@rm -fr vendor/mimalloc/lib/*
## Init and update git submodule
install-submodule:

View File

@@ -196,10 +196,3 @@ 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,17 +45,21 @@ pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const mode = b.standardOptimizeOption(.{});
const options = jsruntime.buildOptions(b);
const options = try jsruntime.buildOptions(b);
const x86 = b.option(bool, "x86", "Use x86 backend") orelse false;
// browser
// -------
// compile and install
const exe = b.addExecutable(.{
.name = "lightpanda",
.name = "browsercore",
.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);
@@ -75,10 +79,12 @@ pub fn build(b: *std.Build) !void {
// compile and install
const shell = b.addExecutable(.{
.name = "lightpanda-shell",
.name = "browsercore-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);
@@ -102,6 +108,8 @@ 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);
@@ -124,10 +132,12 @@ pub fn build(b: *std.Build) !void {
// compile and install
const wpt = b.addExecutable(.{
.name = "lightpanda-wpt",
.name = "browsercore-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);
@@ -139,6 +149,30 @@ 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(
@@ -146,64 +180,38 @@ 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.?,
target,
step.root_module.resolved_target.?,
);
step.root_module.addImport("jsruntime", jsruntimemod);
const netsurf = try moduleNetSurf(b, target);
const netsurf = moduleNetSurf(b);
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, target: std.Build.ResolvedTarget) !*std.Build.Module {
fn moduleNetSurf(b: *std.Build) *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
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));
mod.addObjectFile(b.path("vendor/libiconv/lib/libiconv.a"));
mod.addIncludePath(b.path("vendor/libiconv/include"));
// mimalloc
mod.addImport("mimalloc", (try moduleMimalloc(b, target)));
mod.addImport("mimalloc", moduleMimalloc(b));
// netsurf libs
const ns = "vendor/netsurf";
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));
mod.addIncludePath(b.path(ns ++ "/include"));
const libs: [4][]const u8 = .{
"libdom",
@@ -212,35 +220,20 @@ fn moduleNetSurf(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Mo
"libwapcaplet",
};
inline for (libs) |lib| {
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.addObjectFile(b.path(ns ++ "/lib/" ++ lib ++ ".a"));
mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src"));
}
return mod;
}
fn moduleMimalloc(b: *std.Build, target: std.Build.ResolvedTarget) !*std.Build.Module {
fn moduleMimalloc(b: *std.Build) *std.Build.Module {
const mod = b.addModule("mimalloc", .{
.root_source_file = b.path("src/mimalloc/mimalloc.zig"),
.target = target,
});
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"));
mod.addObjectFile(b.path("vendor/mimalloc/out/libmimalloc.a"));
mod.addIncludePath(b.path("vendor/mimalloc/out/include"));
return mod;
}

View File

@@ -26,7 +26,6 @@ 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;
@@ -39,7 +38,6 @@ pub const Interfaces = generate.Tuple(.{
XHR.Interfaces,
Storage.Interfaces,
URL.Interfaces,
Iterators.Interfaces,
});
pub const UserContext = @import("user_context.zig").UserContext;

1766
src/async/Client.zig Normal file

File diff suppressed because it is too large Load Diff

133
src/async/stream.zig Normal file
View File

@@ -0,0 +1,133 @@
// 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;
}
}
};

112
src/async/tcp.zig Normal file
View File

@@ -0,0 +1,112 @@
// 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,
};
}

189
src/async/test.zig Normal file
View File

@@ -0,0 +1,189 @@
// 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("asyncio").Client;
const HttpClient = @import("../async/Client.zig");
const log = std.log.scoped(.browser);
@@ -49,29 +49,24 @@ const log = std.log.scoped(.browser);
// A browser contains only one session.
// TODO allow multiple sessions per browser.
pub const Browser = struct {
session: Session = undefined,
session: *Session,
const uri = "about:blank";
pub fn init(self: *Browser, alloc: std.mem.Allocator, loop: *Loop, vm: jsruntime.VM) !void {
pub fn init(alloc: std.mem.Allocator, vm: jsruntime.VM) !Browser {
// We want to ensure the caller initialised a VM, but the browser
// doesn't use it directly...
_ = vm;
try Session.init(&self.session, alloc, loop, uri);
return Browser{
.session = try Session.init(alloc, "about:blank"),
};
}
pub fn deinit(self: *Browser) void {
self.session.deinit();
}
pub fn newSession(
self: *Browser,
alloc: std.mem.Allocator,
loop: *jsruntime.Loop,
) !void {
self.session.deinit();
try Session.init(&self.session, alloc, loop, uri);
pub fn currentSession(self: *Browser) *Session {
return self.session;
}
};
@@ -95,37 +90,37 @@ pub const Session = struct {
// TODO handle proxy
loader: Loader,
env: Env = undefined,
inspector: ?jsruntime.Inspector = null,
loop: Loop,
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(self: *Session, alloc: std.mem.Allocator, loop: *Loop, uri: []const u8) !void {
fn init(alloc: std.mem.Allocator, uri: []const u8) !*Session {
var self = try alloc.create(Session);
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,
};
Env.init(&self.env, self.arena.allocator(), loop, null);
self.httpClient = .{ .allocator = alloc };
self.env = try Env.init(self.arena.allocator(), &self.loop, null);
self.httpClient = .{ .allocator = alloc, .loop = &self.loop };
try self.env.load(&self.jstypes);
return self;
}
fn deinit(self: *Session) void {
if (self.page) |*p| p.end();
if (self.inspector) |inspector| {
inspector.deinit(self.alloc);
}
if (self.page) |page| page.end();
self.env.deinit();
self.arena.deinit();
@@ -133,35 +128,12 @@ pub const Session = struct {
self.httpClient.deinit();
self.loader.deinit();
self.storageShed.deinit();
self.loop.deinit();
self.alloc.destroy(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.?;
pub fn createPage(self: *Session) !Page {
return Page.init(self.alloc, self);
}
};
@@ -183,14 +155,16 @@ pub const Page = struct {
raw_data: ?[]const u8 = null,
fn init(
self: *Page,
alloc: std.mem.Allocator,
session: *Session,
) void {
self.* = .{
) !Page {
if (session.page != null) return error.SessionPageExists;
var page = Page{
.arena = std.heap.ArenaAllocator.init(alloc),
.session = session,
};
session.page = &page;
return page;
}
// reset js env and mem arena.
@@ -245,9 +219,7 @@ pub const Page = struct {
}
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
// - auxData: extra data forwarded to the Inspector
// see Inspector.contextCreated
pub fn navigate(self: *Page, uri: []const u8, auxData: ?[]const u8) !void {
pub fn navigate(self: *Page, uri: []const u8) !void {
const alloc = self.arena.allocator();
log.debug("starting GET {s}", .{uri});
@@ -308,7 +280,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", auxData);
try self.loadHTMLDoc(req.reader(), mime.charset orelse "utf-8");
} else {
log.info("non-HTML document: {s}", .{ct.?});
@@ -318,7 +290,7 @@ pub const Page = struct {
}
// https://html.spec.whatwg.org/#read-html
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, auxData: ?[]const u8) !void {
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
const alloc = self.arena.allocator();
// start netsurf memory arena.
@@ -355,11 +327,6 @@ 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,
@@ -444,9 +411,7 @@ 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
@@ -463,9 +428,7 @@ 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
@@ -563,7 +526,7 @@ pub const Page = struct {
const resp = fetchres.req.response;
log.info("fetch script {any}: {d}", .{ u, resp.status });
log.info("fech script {any}: {d}", .{ u, resp.status });
if (resp.status != .ok) return FetchError.BadStatusCode;

View File

@@ -1,148 +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 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);
}

View File

@@ -1,211 +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 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,
};

View File

@@ -1,123 +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 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);
}

View File

@@ -1,59 +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 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);
}

View File

@@ -1,59 +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 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);
}

View File

@@ -1,252 +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");
// 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());
}
}

View File

@@ -1,75 +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 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);
}

View File

@@ -1,452 +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 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 "";
}

View File

@@ -1,59 +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 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);
}

View File

@@ -1,181 +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 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);
}

View File

@@ -1,414 +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 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,6 +29,7 @@ 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");
@@ -434,11 +435,7 @@ pub fn testExecFn(
.{ .src = "document.querySelector(':root').nodeName", .ex = "HTML" },
.{ .src = "document.querySelectorAll('p').length", .ex = "2" },
.{ .src =
\\Array.from(document.querySelectorAll('#content > p#para-empty'))
\\.map(row => row.querySelector('span').textContent)
\\.length;
, .ex = "1" },
.{ .src = "document.querySelectorAll('.ok').item(0).id", .ex = "link" },
};
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");
const NodeList = @import("nodelist.zig").NodeList;
const Nod = @import("node.zig");
const MutationObserver = @import("mutation_observer.zig");
@@ -33,7 +33,7 @@ pub const Interfaces = generate.Tuple(.{
DOMImplementation,
NamedNodeMap,
DOMTokenList,
NodeList.Interfaces,
NodeList,
Nod.Node,
Nod.Interfaces,
MutationObserver.Interfaces,

View File

@@ -40,7 +40,6 @@ 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");
@@ -196,68 +195,21 @@ pub const Node = struct {
return try Node.toInterface(clone);
}
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 _compareDocumentPosition(self: *parser.Node, other: *parser.Node) void {
// TODO
_ = other;
_ = self;
std.log.err("Not implemented {s}", .{"node.compareDocumentPosition()"});
}
pub fn _contains(self: *parser.Node, other: *parser.Node) !bool {
return try parser.nodeContains(self, other);
}
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 _getRootNode(self: *parser.Node) void {
// TODO
_ = self;
std.log.err("Not implemented {s}", .{"node.getRootNode()"});
}
pub fn _hasChildNodes(self: *parser.Node) !bool {
@@ -432,21 +384,6 @@ 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,82 +21,14 @@ 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
@@ -140,50 +72,9 @@ pub const NodeList = struct {
return try Node.toInterface(n);
}
pub fn _forEach(self: *NodeList, alloc: std.mem.Allocator, cbk: Callback) !void { // TODO handle thisArg
var res = CallbackResult.init(alloc);
defer res.deinit();
// TODO _symbol_iterator
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);
}
}
// TODO implement postAttach
};
// Tests
@@ -196,14 +87,6 @@ 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 (null event): {any}", .{e});
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error: {any}", .{e});
}
// in case of function error, we log the result and the trace.
if (!res.success) {
log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
log.info("event handler error: {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(self: *parser.DocumentHTML) !?*parser.Script {
return try parser.documentHTMLGetCurrentScript(self);
pub fn get_currentScript(_: *parser.DocumentHTML) !?*parser.Element {
return null;
}
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {

View File

@@ -96,7 +96,6 @@ pub const Interfaces = .{
HTMLTrackElement,
HTMLUListElement,
HTMLVideoElement,
CSSProperties,
};
const Generated = generate.Union.compile(Interfaces);
pub const Union = Generated._union;
@@ -105,18 +104,10 @@ 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,8 +28,6 @@ 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 {
@@ -68,10 +66,6 @@ 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,3 +1,21 @@
// 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.

View File

@@ -1,35 +0,0 @@
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,367 +17,97 @@
// 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 log = std.log.scoped(.cli);
const socket_path = "/tmp/browsercore-server.sock";
// 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,
var doc: *parser.DocumentHTML = undefined;
var server: std.net.Server = undefined;
/// `undefined` until `listen` returns successfully.
listen_address: std.net.Address,
fn execJS(
alloc: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
// start JS env
try js_env.start();
defer js_env.stop();
sockfd: ?posix.socket_t,
// alias global as self and window
var window = Window.create(null);
window.replaceDocument(doc);
try js_env.bindGlobal(window);
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,
// try catch
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(js_env.*);
defer try_catch.deinit();
/// Enable SO.REUSEADDR on the socket.
reuse_address: bool = false,
while (true) {
/// 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;
// 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;
}
// 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);
}
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});
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);
_ = try conn.stream.write(res_str);
}
/// 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 {
// 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"),
}
}
}
// create v8 vm
const vm = jsruntime.VM.init();
defer vm.deinit();
// 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;
// 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)});
};
defer cli_mode.deinit();
switch (cli_mode) {
.server => |mode| {
// 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;
}
};
// server
var srv = StreamServer.init(.{
.reuse_address = true,
.reuse_port = true,
.nonblocking = true,
});
defer srv.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});
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());
}
},
}
try jsruntime.loadEnv(&arena, null, execJS);
}

97
src/main_get.zig Normal file
View File

@@ -0,0 +1,97 @@
// 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,13 +24,12 @@ 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;
pub const IO = @import("asyncio").Wrapper(jsruntime.Loop);
const Client = @import("async/Client.zig");
var doc: *parser.DocumentHTML = undefined;
@@ -42,7 +41,7 @@ fn execJS(
try js_env.start();
defer js_env.stop();
var cli = Client{ .allocator = alloc };
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
defer cli.deinit();
try js_env.setUserContext(UserContext{
@@ -88,5 +87,5 @@ pub fn main() !void {
defer vm.deinit();
// launch shell
try jsruntime.shell(&arena, execJS, .{ .app_name = "lightpanda-shell" });
try jsruntime.shell(&arena, execJS, .{ .app_name = "browsercore" });
}

View File

@@ -50,12 +50,11 @@ 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 lightpanda will have the html loader, it would be useful to refacto
// Once browsercore 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(.{}){};

View File

@@ -1,172 +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");
/// 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(u32) {
pub const DocumentPosition = enum(u2) {
disconnected = c.DOM_DOCUMENT_POSITION_DISCONNECTED,
preceding = c.DOM_DOCUMENT_POSITION_PRECEDING,
following = c.DOM_DOCUMENT_POSITION_FOLLOWING,
@@ -2249,18 +2249,3 @@ 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("asyncio").Client;
const Client = @import("async/Client.zig");
const documentTestExecFn = @import("dom/document.zig").testExecFn;
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
@@ -59,7 +59,6 @@ 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;
@@ -87,7 +86,7 @@ fn testExecFn(
std.debug.print("documentHTMLClose error: {s}\n", .{@errorName(err)});
};
var cli = Client{ .allocator = alloc };
var cli = Client{ .allocator = alloc, .loop = js_env.nat_ctx.loop };
defer cli.deinit();
try js_env.setUserContext(.{
@@ -284,7 +283,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 lightpanda 🚀", header);
var t = table.init("Benchmark browsercore 🚀", 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.
@@ -296,8 +295,8 @@ const kb = 1024;
const ms = std.time.ns_per_ms;
test {
const msgTest = @import("msg.zig");
std.testing.refAllDecls(msgTest);
const asyncTest = @import("async/test.zig");
std.testing.refAllDecls(asyncTest);
const dumpTest = @import("browser/dump.zig");
std.testing.refAllDecls(dumpTest);
@@ -319,8 +318,6 @@ 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 {

View File

@@ -1,499 +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 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,7 +22,6 @@ 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("asyncio").Client;
const Client = @import("async/Client.zig");
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,11 +53,10 @@ 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 };
var cli = Client{ .allocator = alloc, .loop = &loop };
defer cli.deinit();
var js_env: Env = undefined;
Env.init(&js_env, alloc, &loop, UserContext{
var js_env = try Env.init(alloc, &loop, UserContext{
.document = html_doc,
.httpClient = &cli,
});

View File

@@ -32,7 +32,8 @@ const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEven
const Mime = @import("../browser/mime.zig");
const Loop = jsruntime.Loop;
const Client = @import("asyncio").Client;
const YieldImpl = Loop.Yield(XMLHttpRequest);
const Client = @import("../async/Client.zig");
const parser = @import("netsurf");
@@ -97,11 +98,10 @@ pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator,
cli: *Client,
io: Client.IO,
impl: YieldImpl,
priv_state: PrivState = .new,
req: ?Client.Request = null,
ctx: ?Client.Ctx = null,
method: std.http.Method,
state: u16,
@@ -135,13 +135,7 @@ pub const XMLHttpRequest = struct {
response_header_buffer: [1024 * 16]u8 = undefined,
response_status: u10 = 0,
// 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_override_mime_type: ?[]const u8 = null,
response_mime: Mime = undefined,
response_obj: ?ResponseObj = null,
send_flag: bool = false,
@@ -294,7 +288,7 @@ pub const XMLHttpRequest = struct {
.alloc = alloc,
.headers = Headers.init(alloc),
.response_headers = Headers.init(alloc),
.io = Client.IO.init(loop),
.impl = YieldImpl.init(loop),
.method = undefined,
.url = null,
.uri = undefined,
@@ -326,11 +320,10 @@ pub const XMLHttpRequest = struct {
self.priv_state = .new;
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
if (self.req) |*r| {
r.deinit();
self.req = null;
}
}
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
@@ -389,11 +382,7 @@ pub const XMLHttpRequest = struct {
self.reset(alloc);
self.url = try alloc.dupe(u8, url);
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.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax;
self.sync = if (asyn) |b| !b else false;
self.state = OPENED;
@@ -505,160 +494,138 @@ pub const XMLHttpRequest = struct {
log.debug("{any} {any}", .{ self.method, self.uri });
self.send_flag = true;
self.impl.yield(self);
}
self.priv_state = .open;
// 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.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;
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.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));
self.impl.yield(self);
}
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;
@@ -668,12 +635,6 @@ 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 {
@@ -921,7 +882,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', 'https://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
// ensure open resets values
@@ -951,7 +912,7 @@ pub fn testExecFn(
var document = [_]Case{
.{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req2.open('GET', 'https://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req2.open('GET', 'http://httpbin.io/html')", .ex = "undefined" },
.{ .src = "req2.responseType = 'document'", .ex = "document" },
.{ .src = "req2.send()", .ex = "undefined" },
@@ -967,7 +928,7 @@ pub fn testExecFn(
var json = [_]Case{
.{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req3.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req3.responseType = 'json'", .ex = "json" },
.{ .src = "req3.send()", .ex = "undefined" },
@@ -982,7 +943,7 @@ pub fn testExecFn(
var post = [_]Case{
.{ .src = "const req4 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req4.open('POST', 'https://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req4.open('POST', 'http://httpbin.io/post')", .ex = "undefined" },
.{ .src = "req4.send('foo')", .ex = "undefined" },
// Each case executed waits for all loop callaback calls.
@@ -995,7 +956,7 @@ pub fn testExecFn(
var cbk = [_]Case{
.{ .src = "const req5 = new XMLHttpRequest()", .ex = "undefined" },
.{ .src = "req5.open('GET', 'https://httpbin.io/json')", .ex = "undefined" },
.{ .src = "req5.open('GET', 'http://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 vendor/zig-async-io deleted from ed7ae07d1c