camoufox 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +24 -0
- data/ext/camoufox_native/Makefile +267 -0
- data/ext/camoufox_native/camoufox_native.cpp +57 -2
- data/lib/camoufox/__version__.rb +1 -1
- data/lib/camoufox/native_bridge.rb +6 -1
- data/lib/camoufox/syncSession.js +251 -0
- data/lib/camoufox/sync_api.rb +192 -31
- data/lib/camoufox_native.bundle +0 -0
- metadata +4 -3
- data/lib/camoufox/visit.js +0 -129
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fed67395114c5d9ecb19c0c4568266f4870f16927b9fbb7dbe20419bade6978
|
|
4
|
+
data.tar.gz: 45a0a6c1d40ca0988061d80f5fccdb8b2dbc711c7249971551355b8e5ab66655
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37b227afb8200918e51019dae5f58cbf086729ac15e76bbecfe52f6f1c4a5f1a59a5127dd62368caaead14e7493124ac76e3c794a35904b8be94cb3fd1736dc4
|
|
7
|
+
data.tar.gz: ae7e5b475c690f3b646a1e78a50134c2d27830e8bf63d4fff35f5aec328327a23f89703d1028f0fd1f6b2d5376067526fbe33edccb8c02d7f094f33af1cab73f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
- Add `user_data_dir` support to the synchronous Playwright bridge so Firefox can reuse persistent
|
|
5
|
+
profiles via `launchPersistentContext`.
|
|
6
|
+
|
|
7
|
+
## 0.4.2
|
|
8
|
+
- Default the native stub's `executable_path` to `File.join(Camoufox::Pkgman.install_dir, "camoufox")`
|
|
9
|
+
while still honoring kwargs or `CAMOUFOX_EXECUTABLE_PATH`, so Playwright can launch whichever
|
|
10
|
+
Camoufox binary the gem installed.
|
|
11
|
+
|
|
12
|
+
## 0.4.0
|
|
13
|
+
- Rework the synchronous Playwright bridge to keep a persistent Firefox page session so multiple
|
|
14
|
+
commands run against the same browser instance.
|
|
15
|
+
- Add `Camoufox::SyncAPI::Page#wait_for_selector` and expose live `title`/`content` reads through
|
|
16
|
+
the new bridge.
|
|
17
|
+
- Allow the native stub to honor `executable_path` kwargs or `CAMOUFOX_EXECUTABLE_PATH` so the
|
|
18
|
+
Playwright bridge can launch real binaries without patching the extension.
|
|
19
|
+
|
|
3
20
|
## 0.3.0
|
|
4
21
|
- Improve Playwright bridge: unwrap driver bundles that only expose `createPlaywright`/`default`
|
|
5
22
|
exports so `Camoufox::SyncAPI` can always reach `playwright.firefox`.
|
data/README.md
CHANGED
|
@@ -48,7 +48,9 @@ end
|
|
|
48
48
|
Camoufox::SyncAPI::Camoufox.open(headless: true) do |browser|
|
|
49
49
|
page = browser.new_page
|
|
50
50
|
page.goto("https://example.com")
|
|
51
|
+
page.wait_for_selector('h1')
|
|
51
52
|
puts page.title
|
|
53
|
+
puts page.content.include?('Example Domain')
|
|
52
54
|
end
|
|
53
55
|
```
|
|
54
56
|
|
|
@@ -56,6 +58,27 @@ Behind the scenes the Ruby port encodes the Camoufox launch options, hands them
|
|
|
56
58
|
bridge, and lets Playwright do the heavy lifting. You must supply a Playwright driver bundle or
|
|
57
59
|
installation so the Node script can `require('playwright')`.
|
|
58
60
|
|
|
61
|
+
The synchronous helper keeps a Firefox page alive for the lifetime of the Ruby object, so follow-up
|
|
62
|
+
calls like `wait_for_selector`, `content`, or `title` reuse the same DOM state without re-launching
|
|
63
|
+
the browser for every method.
|
|
64
|
+
|
|
65
|
+
### Reusing Firefox profiles
|
|
66
|
+
|
|
67
|
+
Playwright's `launchPersistentContext` API can now be toggled by passing a `user_data_dir` when
|
|
68
|
+
opening a Camoufox session. The directory stores cookies, history, and other profile data between
|
|
69
|
+
runs, mirroring how Playwright persists Chromium profiles:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Camoufox::SyncAPI::Camoufox.open(headless: false, user_data_dir: "/tmp/camoufox-profile") do |browser|
|
|
73
|
+
page = browser.new_page
|
|
74
|
+
page.goto("https://example.com")
|
|
75
|
+
puts page.title
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When `user_data_dir` is provided the Node bridge launches Firefox through
|
|
80
|
+
`browserType.launchPersistentContext`, so every browser command reuses the same profile directory.
|
|
81
|
+
|
|
59
82
|
### Launching the Playwright server (experimental)
|
|
60
83
|
|
|
61
84
|
To mirror the Python helper that spins up a Playwright websocket endpoint, the Ruby port can invoke
|
|
@@ -138,6 +161,7 @@ Environment overrides:
|
|
|
138
161
|
|
|
139
162
|
- `CAMOUFOX_DATA_DIR` – override where Camoufox assets are stored (planned use)
|
|
140
163
|
- `CAMOUFOX_CACHE_DIR` – override the cache directory (planned use)
|
|
164
|
+
- `CAMOUFOX_EXECUTABLE_PATH` – path to the Camoufox Firefox binary returned by the native stub (defaults to `File.join(Camoufox::Pkgman.install_dir, "camoufox")`, but override it if you place the browser elsewhere)
|
|
141
165
|
- `CAMOUFOX_NODE_PATH` – path to the Node.js binary used when spawning the Playwright server (defaults to `node`)
|
|
142
166
|
- `CAMOUFOX_PLAYWRIGHT_DRIVER_DIR` – directory containing `lib/browserServerImpl.js` (defaults to `node_modules/playwright` if present)
|
|
143
167
|
- `CAMOUFOX_PLAYWRIGHT_JS_REQUIRE` – optional module identifier passed to Node's `require()` when
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
|
|
2
|
+
SHELL = /bin/sh
|
|
3
|
+
|
|
4
|
+
# V=0 quiet, V=1 verbose. other values don't work.
|
|
5
|
+
V = 0
|
|
6
|
+
Q1 = $(V:1=)
|
|
7
|
+
Q = $(Q1:0=@)
|
|
8
|
+
ECHO1 = $(V:1=@ :)
|
|
9
|
+
ECHO = $(ECHO1:0=@ echo)
|
|
10
|
+
NULLCMD = :
|
|
11
|
+
|
|
12
|
+
#### Start of system configuration section. ####
|
|
13
|
+
|
|
14
|
+
srcdir = .
|
|
15
|
+
topdir = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0
|
|
16
|
+
hdrdir = $(topdir)
|
|
17
|
+
arch_hdrdir = /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/universal-darwin24
|
|
18
|
+
PATH_SEPARATOR = :
|
|
19
|
+
VPATH = $(srcdir):$(arch_hdrdir)/ruby:$(hdrdir)/ruby
|
|
20
|
+
prefix = $(DESTDIR)/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr
|
|
21
|
+
rubysitearchprefix = $(rubylibprefix)/$(sitearch)
|
|
22
|
+
rubyarchprefix = $(rubylibprefix)/$(arch)
|
|
23
|
+
rubylibprefix = $(libdir)/$(RUBY_BASE_NAME)
|
|
24
|
+
exec_prefix = $(prefix)
|
|
25
|
+
vendorarchhdrdir = $(vendorhdrdir)/$(sitearch)
|
|
26
|
+
sitearchhdrdir = $(sitehdrdir)/$(sitearch)
|
|
27
|
+
rubyarchhdrdir = $(rubyhdrdir)/$(arch)
|
|
28
|
+
vendorhdrdir = $(rubyhdrdir)/vendor_ruby
|
|
29
|
+
sitehdrdir = $(rubyhdrdir)/site_ruby
|
|
30
|
+
rubyhdrdir = $(includedir)/$(RUBY_VERSION_NAME)
|
|
31
|
+
vendorarchdir = $(vendorlibdir)/$(sitearch)
|
|
32
|
+
vendorlibdir = $(vendordir)/$(ruby_version)
|
|
33
|
+
vendordir = $(rubylibprefix)/vendor_ruby
|
|
34
|
+
sitearchdir = $(sitelibdir)/$(sitearch)
|
|
35
|
+
sitelibdir = $(sitedir)/$(ruby_version)
|
|
36
|
+
sitedir = $(DESTDIR)/Library/Ruby/Site
|
|
37
|
+
rubyarchdir = $(rubylibdir)/$(arch)
|
|
38
|
+
rubylibdir = $(rubylibprefix)/$(ruby_version)
|
|
39
|
+
sitearchincludedir = $(includedir)/$(sitearch)
|
|
40
|
+
archincludedir = $(includedir)/$(arch)
|
|
41
|
+
sitearchlibdir = $(libdir)/$(sitearch)
|
|
42
|
+
archlibdir = $(libdir)/$(arch)
|
|
43
|
+
ridir = $(datarootdir)/$(RI_BASE_NAME)
|
|
44
|
+
mandir = $(DESTDIR)/usr/share/man
|
|
45
|
+
localedir = $(datarootdir)/locale
|
|
46
|
+
libdir = $(exec_prefix)/lib
|
|
47
|
+
psdir = $(docdir)
|
|
48
|
+
pdfdir = $(docdir)
|
|
49
|
+
dvidir = $(docdir)
|
|
50
|
+
htmldir = $(docdir)
|
|
51
|
+
infodir = $(DESTDIR)/usr/share/info
|
|
52
|
+
docdir = $(datarootdir)/doc/$(PACKAGE)
|
|
53
|
+
oldincludedir = $(DESTDIR)/usr/include
|
|
54
|
+
includedir = $(DESTDIR)/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.1.sdk$(prefix)/include
|
|
55
|
+
runstatedir = $(localstatedir)/run
|
|
56
|
+
localstatedir = $(prefix)/var
|
|
57
|
+
sharedstatedir = $(prefix)/com
|
|
58
|
+
sysconfdir = $(DESTDIR)/Library/Ruby/Site
|
|
59
|
+
datadir = $(datarootdir)
|
|
60
|
+
datarootdir = $(prefix)/share
|
|
61
|
+
libexecdir = $(exec_prefix)/libexec
|
|
62
|
+
sbindir = $(exec_prefix)/sbin
|
|
63
|
+
bindir = $(exec_prefix)/bin
|
|
64
|
+
archdir = $(rubyarchdir)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
CC_WRAPPER =
|
|
68
|
+
CC = xcrun clang
|
|
69
|
+
CXX = xcrun clang++
|
|
70
|
+
LIBRUBY = $(LIBRUBY_SO)
|
|
71
|
+
LIBRUBY_A = lib$(RUBY_SO_NAME)-static.a
|
|
72
|
+
LIBRUBYARG_SHARED = -l$(RUBY_SO_NAME)
|
|
73
|
+
LIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)-static -framework Security -framework Foundation $(MAINLIBS)
|
|
74
|
+
empty =
|
|
75
|
+
OUTFLAG = -o $(empty)
|
|
76
|
+
COUTFLAG = -o $(empty)
|
|
77
|
+
CSRCFLAG = $(empty)
|
|
78
|
+
|
|
79
|
+
RUBY_EXTCONF_H =
|
|
80
|
+
cflags = $(optflags) $(debugflags) $(warnflags)
|
|
81
|
+
cxxflags = $(optflags) $(debugflags) $(warnflags)
|
|
82
|
+
optflags =
|
|
83
|
+
debugflags = -g
|
|
84
|
+
warnflags =
|
|
85
|
+
cppflags =
|
|
86
|
+
CCDLFLAGS =
|
|
87
|
+
CFLAGS = $(CCDLFLAGS) -g -Os -pipe -DHAVE_GCC_ATOMIC_BUILTINS -fno-typed-memory-operations -fno-typed-cxx-new-delete -DUSE_FFI_CLOSURE_ALLOC -std=c99 $(ARCH_FLAG)
|
|
88
|
+
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir)
|
|
89
|
+
DEFS =
|
|
90
|
+
CPPFLAGS = -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT $(DEFS) $(cppflags)
|
|
91
|
+
CXXFLAGS = $(CCDLFLAGS) -g -Os -pipe -std=c++20 -Wall -Wextra $(ARCH_FLAG)
|
|
92
|
+
ldflags = -L. -L/AppleInternal/Library/BuildRoots/4~B5FAugAunRW9OK7YcFNXDBTTVy6DXTYoxzE3ORQ/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.6.Internal.sdk/usr/local/lib
|
|
93
|
+
dldflags = $(ARCH_FLAG) -undefined dynamic_lookup
|
|
94
|
+
ARCH_FLAG =
|
|
95
|
+
DLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)
|
|
96
|
+
LDSHARED = $(CC) -dynamic -bundle
|
|
97
|
+
LDSHAREDXX = $(CXX) -dynamic -bundle
|
|
98
|
+
AR = libtool -static
|
|
99
|
+
EXEEXT =
|
|
100
|
+
|
|
101
|
+
RUBY_INSTALL_NAME = $(RUBY_BASE_NAME)
|
|
102
|
+
RUBY_SO_NAME = ruby.2.6
|
|
103
|
+
RUBYW_INSTALL_NAME =
|
|
104
|
+
RUBY_VERSION_NAME = $(RUBY_BASE_NAME)-$(ruby_version)
|
|
105
|
+
RUBYW_BASE_NAME = rubyw
|
|
106
|
+
RUBY_BASE_NAME = ruby
|
|
107
|
+
|
|
108
|
+
arch = universal-darwin24
|
|
109
|
+
sitearch = $(arch)
|
|
110
|
+
ruby_version = 2.6.0
|
|
111
|
+
ruby = $(bindir)/$(RUBY_BASE_NAME)
|
|
112
|
+
RUBY = $(ruby)
|
|
113
|
+
ruby_headers = $(hdrdir)/ruby.h $(hdrdir)/ruby/backward.h $(hdrdir)/ruby/ruby.h $(hdrdir)/ruby/defines.h $(hdrdir)/ruby/missing.h $(hdrdir)/ruby/intern.h $(hdrdir)/ruby/st.h $(hdrdir)/ruby/subst.h $(arch_hdrdir)/ruby/config.h
|
|
114
|
+
|
|
115
|
+
RM = rm -f
|
|
116
|
+
RM_RF = $(RUBY) -run -e rm -- -rf
|
|
117
|
+
RMDIRS = rmdir -p
|
|
118
|
+
MAKEDIRS = mkdir -p
|
|
119
|
+
INSTALL = /usr/bin/install -c
|
|
120
|
+
INSTALL_PROG = $(INSTALL) -m 0755
|
|
121
|
+
INSTALL_DATA = $(INSTALL) -m 644
|
|
122
|
+
COPY = cp
|
|
123
|
+
TOUCH = exit >
|
|
124
|
+
|
|
125
|
+
#### End of system configuration section. ####
|
|
126
|
+
|
|
127
|
+
preload =
|
|
128
|
+
libpath = . $(libdir)
|
|
129
|
+
LIBPATH = -L. -L$(libdir)
|
|
130
|
+
DEFFILE =
|
|
131
|
+
|
|
132
|
+
CLEANFILES = mkmf.log
|
|
133
|
+
DISTCLEANFILES =
|
|
134
|
+
DISTCLEANDIRS =
|
|
135
|
+
|
|
136
|
+
extout =
|
|
137
|
+
extout_prefix =
|
|
138
|
+
target_prefix =
|
|
139
|
+
LOCAL_LIBS =
|
|
140
|
+
LIBS = $(LIBRUBYARG_SHARED)
|
|
141
|
+
ORIG_SRCS = camoufox_native.cpp
|
|
142
|
+
SRCS = $(ORIG_SRCS)
|
|
143
|
+
OBJS = camoufox_native.o
|
|
144
|
+
HDRS =
|
|
145
|
+
LOCAL_HDRS =
|
|
146
|
+
TARGET = camoufox_native
|
|
147
|
+
TARGET_NAME = camoufox_native
|
|
148
|
+
TARGET_ENTRY = Init_$(TARGET_NAME)
|
|
149
|
+
DLLIB = $(TARGET).bundle
|
|
150
|
+
EXTSTATIC =
|
|
151
|
+
STATIC_LIB =
|
|
152
|
+
|
|
153
|
+
TIMESTAMP_DIR = .
|
|
154
|
+
BINDIR = $(bindir)
|
|
155
|
+
RUBYCOMMONDIR = $(sitedir)$(target_prefix)
|
|
156
|
+
RUBYLIBDIR = $(sitelibdir)$(target_prefix)
|
|
157
|
+
RUBYARCHDIR = $(sitearchdir)$(target_prefix)
|
|
158
|
+
HDRDIR = $(rubyhdrdir)/ruby$(target_prefix)
|
|
159
|
+
ARCHHDRDIR = $(rubyhdrdir)/$(arch)/ruby$(target_prefix)
|
|
160
|
+
TARGET_SO_DIR =
|
|
161
|
+
TARGET_SO = $(TARGET_SO_DIR)$(DLLIB)
|
|
162
|
+
CLEANLIBS = $(TARGET_SO)
|
|
163
|
+
CLEANOBJS = *.o *.bak
|
|
164
|
+
|
|
165
|
+
all: $(DLLIB)
|
|
166
|
+
static: $(STATIC_LIB)
|
|
167
|
+
.PHONY: all install static install-so install-rb
|
|
168
|
+
.PHONY: clean clean-so clean-static clean-rb
|
|
169
|
+
|
|
170
|
+
clean-static::
|
|
171
|
+
clean-rb-default::
|
|
172
|
+
clean-rb::
|
|
173
|
+
clean-so::
|
|
174
|
+
clean: clean-so clean-static clean-rb-default clean-rb
|
|
175
|
+
-$(Q)$(RM) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES) .*.time
|
|
176
|
+
|
|
177
|
+
distclean-rb-default::
|
|
178
|
+
distclean-rb::
|
|
179
|
+
distclean-so::
|
|
180
|
+
distclean-static::
|
|
181
|
+
distclean: clean distclean-so distclean-static distclean-rb-default distclean-rb
|
|
182
|
+
-$(Q)$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
|
|
183
|
+
-$(Q)$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)
|
|
184
|
+
-$(Q)$(RMDIRS) $(DISTCLEANDIRS) 2> /dev/null || true
|
|
185
|
+
|
|
186
|
+
realclean: distclean
|
|
187
|
+
install: install-so install-rb
|
|
188
|
+
|
|
189
|
+
install-so: $(DLLIB) $(TIMESTAMP_DIR)/.sitearchdir.time
|
|
190
|
+
$(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)
|
|
191
|
+
clean-static::
|
|
192
|
+
-$(Q)$(RM) $(STATIC_LIB)
|
|
193
|
+
install-rb: pre-install-rb do-install-rb install-rb-default
|
|
194
|
+
install-rb-default: pre-install-rb-default do-install-rb-default
|
|
195
|
+
pre-install-rb: Makefile
|
|
196
|
+
pre-install-rb-default: Makefile
|
|
197
|
+
do-install-rb:
|
|
198
|
+
do-install-rb-default:
|
|
199
|
+
pre-install-rb-default:
|
|
200
|
+
@$(NULLCMD)
|
|
201
|
+
$(TIMESTAMP_DIR)/.sitearchdir.time:
|
|
202
|
+
$(Q) $(MAKEDIRS) $(@D) $(RUBYARCHDIR)
|
|
203
|
+
$(Q) $(TOUCH) $@
|
|
204
|
+
|
|
205
|
+
site-install: site-install-so site-install-rb
|
|
206
|
+
site-install-so: install-so
|
|
207
|
+
site-install-rb: install-rb
|
|
208
|
+
|
|
209
|
+
.SUFFIXES: .c .m .cc .mm .cxx .cpp .o .S
|
|
210
|
+
|
|
211
|
+
.cc.o:
|
|
212
|
+
$(ECHO) compiling $(<)
|
|
213
|
+
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
|
214
|
+
|
|
215
|
+
.cc.S:
|
|
216
|
+
$(ECHO) translating $(<)
|
|
217
|
+
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
|
218
|
+
|
|
219
|
+
.mm.o:
|
|
220
|
+
$(ECHO) compiling $(<)
|
|
221
|
+
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
|
222
|
+
|
|
223
|
+
.mm.S:
|
|
224
|
+
$(ECHO) translating $(<)
|
|
225
|
+
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
|
226
|
+
|
|
227
|
+
.cxx.o:
|
|
228
|
+
$(ECHO) compiling $(<)
|
|
229
|
+
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
|
230
|
+
|
|
231
|
+
.cxx.S:
|
|
232
|
+
$(ECHO) translating $(<)
|
|
233
|
+
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
|
234
|
+
|
|
235
|
+
.cpp.o:
|
|
236
|
+
$(ECHO) compiling $(<)
|
|
237
|
+
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
|
238
|
+
|
|
239
|
+
.cpp.S:
|
|
240
|
+
$(ECHO) translating $(<)
|
|
241
|
+
$(Q) $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
|
242
|
+
|
|
243
|
+
.c.o:
|
|
244
|
+
$(ECHO) compiling $(<)
|
|
245
|
+
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
|
246
|
+
|
|
247
|
+
.c.S:
|
|
248
|
+
$(ECHO) translating $(<)
|
|
249
|
+
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
|
250
|
+
|
|
251
|
+
.m.o:
|
|
252
|
+
$(ECHO) compiling $(<)
|
|
253
|
+
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$<
|
|
254
|
+
|
|
255
|
+
.m.S:
|
|
256
|
+
$(ECHO) translating $(<)
|
|
257
|
+
$(Q) $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -S $(CSRCFLAG)$<
|
|
258
|
+
|
|
259
|
+
$(TARGET_SO): $(OBJS) Makefile
|
|
260
|
+
$(ECHO) linking shared-object $(DLLIB)
|
|
261
|
+
-$(Q)$(RM) $(@)
|
|
262
|
+
$(Q) $(LDSHAREDXX) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)
|
|
263
|
+
$(Q) $(POSTLINK)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
$(OBJS): $(HDRS) $(ruby_headers)
|
|
@@ -1,13 +1,61 @@
|
|
|
1
1
|
#include <ruby.h>
|
|
2
2
|
#include <cstring>
|
|
3
|
+
#include <cstdlib>
|
|
3
4
|
|
|
4
5
|
namespace {
|
|
5
6
|
|
|
7
|
+
VALUE pkgman_default_executable() {
|
|
8
|
+
ID camoufox_id = rb_intern("Camoufox");
|
|
9
|
+
if (!rb_const_defined(rb_cObject, camoufox_id)) {
|
|
10
|
+
return Qnil;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
VALUE camoufox_module = rb_const_get(rb_cObject, camoufox_id);
|
|
14
|
+
ID pkgman_id = rb_intern("Pkgman");
|
|
15
|
+
if (!rb_const_defined(camoufox_module, pkgman_id)) {
|
|
16
|
+
return Qnil;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
VALUE pkgman_module = rb_const_get(camoufox_module, pkgman_id);
|
|
20
|
+
VALUE install_dir = rb_funcall(pkgman_module, rb_intern("install_dir"), 0);
|
|
21
|
+
if (NIL_P(install_dir)) {
|
|
22
|
+
return Qnil;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
VALUE file_class = rb_const_get(rb_cObject, rb_intern("File"));
|
|
26
|
+
VALUE executable_name = rb_str_new_cstr("camoufox");
|
|
27
|
+
return rb_funcall(file_class, rb_intern("join"), 2, install_dir, executable_name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
VALUE fetch_executable_path(VALUE rb_options) {
|
|
31
|
+
ID executable_id = rb_intern("executable_path");
|
|
32
|
+
VALUE executable_key = ID2SYM(executable_id);
|
|
33
|
+
VALUE explicit_value = rb_hash_lookup(rb_options, executable_key);
|
|
34
|
+
|
|
35
|
+
if (!NIL_P(explicit_value)) {
|
|
36
|
+
return explicit_value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const char* env_value = std::getenv("CAMOUFOX_EXECUTABLE_PATH");
|
|
40
|
+
if (env_value && env_value[0] != '\0') {
|
|
41
|
+
return rb_str_new_cstr(env_value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
VALUE pkgman_path = pkgman_default_executable();
|
|
45
|
+
if (!NIL_P(pkgman_path)) {
|
|
46
|
+
return pkgman_path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Qnil;
|
|
50
|
+
}
|
|
51
|
+
|
|
6
52
|
VALUE build_stub_launch_options(VALUE rb_options) {
|
|
53
|
+
Check_Type(rb_options, T_HASH);
|
|
7
54
|
VALUE result = rb_hash_new();
|
|
8
55
|
|
|
9
|
-
VALUE
|
|
10
|
-
|
|
56
|
+
VALUE executable_key = ID2SYM(rb_intern("executable_path"));
|
|
57
|
+
VALUE executable_path = fetch_executable_path(rb_options);
|
|
58
|
+
rb_hash_aset(result, executable_key, executable_path);
|
|
11
59
|
|
|
12
60
|
VALUE args = rb_ary_new();
|
|
13
61
|
rb_hash_aset(result, ID2SYM(rb_intern("args")), args);
|
|
@@ -28,6 +76,13 @@ VALUE build_stub_launch_options(VALUE rb_options) {
|
|
|
28
76
|
|
|
29
77
|
rb_hash_aset(result, headless_key, headless_value);
|
|
30
78
|
|
|
79
|
+
ID user_data_dir_id = rb_intern("user_data_dir");
|
|
80
|
+
VALUE user_data_dir_key = ID2SYM(user_data_dir_id);
|
|
81
|
+
VALUE user_data_dir_value = rb_hash_lookup(rb_options, user_data_dir_key);
|
|
82
|
+
if (!NIL_P(user_data_dir_value)) {
|
|
83
|
+
rb_hash_aset(result, user_data_dir_key, user_data_dir_value);
|
|
84
|
+
}
|
|
85
|
+
|
|
31
86
|
return result;
|
|
32
87
|
}
|
|
33
88
|
|
data/lib/camoufox/__version__.rb
CHANGED
|
@@ -15,7 +15,12 @@ module Camoufox
|
|
|
15
15
|
|
|
16
16
|
def launch_options(**kwargs)
|
|
17
17
|
ensure_loaded!
|
|
18
|
-
CamoufoxNative.launch_options(kwargs)
|
|
18
|
+
options = CamoufoxNative.launch_options(kwargs)
|
|
19
|
+
if kwargs.key?(:user_data_dir)
|
|
20
|
+
options = options.dup
|
|
21
|
+
options[:user_data_dir] = kwargs[:user_data_dir]
|
|
22
|
+
end
|
|
23
|
+
options
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def run_cli(command, args = [])
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
function withDefault(value, extractor) {
|
|
6
|
+
if (!value) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
return extractor(value);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.warn(`camoufox: failed to initialize Playwright shim (${error.message || error})`);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolvePlaywrightApi(candidate) {
|
|
19
|
+
if (!candidate) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (candidate.firefox) {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (candidate.default) {
|
|
28
|
+
const resolved = resolvePlaywrightApi(candidate.default);
|
|
29
|
+
if (resolved) {
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof candidate.createInProcessPlaywright === 'function') {
|
|
35
|
+
const resolved = withDefault(candidate, (mod) => mod.createInProcessPlaywright());
|
|
36
|
+
if (resolved) {
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof candidate.createPlaywright === 'function') {
|
|
42
|
+
const resolved = withDefault(candidate, (mod) => mod.createPlaywright({ sdkLanguage: process.env.PW_LANG_NAME || 'javascript' }));
|
|
43
|
+
if (resolved && resolved.firefox) {
|
|
44
|
+
return resolved;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (candidate.playwright) {
|
|
49
|
+
return resolvePlaywrightApi(candidate.playwright);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function loadPlaywright() {
|
|
56
|
+
const override = process.env.CAMOUFOX_PLAYWRIGHT_JS_REQUIRE;
|
|
57
|
+
if (override) {
|
|
58
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
59
|
+
return require(override);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
64
|
+
return require('playwright');
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// fall through
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const driverDir = process.env.CAMOUFOX_PLAYWRIGHT_DRIVER_DIR;
|
|
70
|
+
if (driverDir) {
|
|
71
|
+
try {
|
|
72
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
73
|
+
return require(path.join(driverDir, 'package'));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// fall through
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.error('Unable to require Playwright. Install the `playwright` npm package or set CAMOUFOX_PLAYWRIGHT_JS_REQUIRE.');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function decodePayload(line) {
|
|
84
|
+
const trimmed = (line || '').trim();
|
|
85
|
+
if (!trimmed) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const buffer = Buffer.from(trimmed, 'base64');
|
|
89
|
+
return JSON.parse(buffer.toString());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function encodePayload(message) {
|
|
93
|
+
const json = JSON.stringify(message);
|
|
94
|
+
return Buffer.from(json).toString('base64');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function writeMessage(message) {
|
|
98
|
+
process.stdout.write(`${encodePayload(message)}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function waitForNetworkIdle(page, timeout) {
|
|
102
|
+
try {
|
|
103
|
+
await page.waitForLoadState('networkidle', { timeout });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn(`camoufox: waitForLoadState(networkidle) warning: ${error.message || error}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function run() {
|
|
110
|
+
const rl = readline.createInterface({
|
|
111
|
+
input: process.stdin,
|
|
112
|
+
crlfDelay: Infinity,
|
|
113
|
+
terminal: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let initialized = false;
|
|
117
|
+
let browser;
|
|
118
|
+
let context;
|
|
119
|
+
let page;
|
|
120
|
+
|
|
121
|
+
rl.on('line', async (line) => {
|
|
122
|
+
try {
|
|
123
|
+
if (!initialized) {
|
|
124
|
+
const payload = decodePayload(line);
|
|
125
|
+
if (!payload || !payload.options) {
|
|
126
|
+
throw new Error('Missing launch options');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const options = payload.options;
|
|
130
|
+
if (options.executablePath && !fs.existsSync(options.executablePath)) {
|
|
131
|
+
delete options.executablePath;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const playwrightModule = loadPlaywright();
|
|
135
|
+
const playwright = resolvePlaywrightApi(playwrightModule);
|
|
136
|
+
const browserType = playwright && playwright.firefox;
|
|
137
|
+
if (!browserType) {
|
|
138
|
+
throw new Error('Playwright module does not expose `firefox` browser type');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const userDataDir = options.userDataDir;
|
|
142
|
+
if (userDataDir) {
|
|
143
|
+
delete options.userDataDir;
|
|
144
|
+
context = await browserType.launchPersistentContext(userDataDir, options);
|
|
145
|
+
const pages = context.pages();
|
|
146
|
+
page = pages.length ? pages[0] : await context.newPage();
|
|
147
|
+
} else {
|
|
148
|
+
browser = await browserType.launch(options);
|
|
149
|
+
page = await browser.newPage();
|
|
150
|
+
}
|
|
151
|
+
initialized = true;
|
|
152
|
+
writeMessage({ event: 'ready' });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const payload = decodePayload(line);
|
|
157
|
+
if (!payload || typeof payload !== 'object') {
|
|
158
|
+
throw new Error('Invalid command payload');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { id, action, params = {} } = payload;
|
|
162
|
+
if (typeof id === 'undefined') {
|
|
163
|
+
throw new Error('Command is missing id');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const respond = (body) => writeMessage({ id, ...body });
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
let result;
|
|
170
|
+
switch (action) {
|
|
171
|
+
case 'goto': {
|
|
172
|
+
const { url, waitUntil = 'domcontentloaded', waitForNetworkIdleTimeout = 15000 } = params;
|
|
173
|
+
if (!url) {
|
|
174
|
+
throw new Error('goto requires a url');
|
|
175
|
+
}
|
|
176
|
+
await page.goto(url, { waitUntil });
|
|
177
|
+
await waitForNetworkIdle(page, waitForNetworkIdleTimeout);
|
|
178
|
+
const [title, content] = await Promise.all([page.title(), page.content()]);
|
|
179
|
+
result = { title, content };
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'wait_for_selector': {
|
|
183
|
+
const { selector, options = {} } = params;
|
|
184
|
+
if (!selector) {
|
|
185
|
+
throw new Error('wait_for_selector requires a selector');
|
|
186
|
+
}
|
|
187
|
+
const handle = await page.waitForSelector(selector, options);
|
|
188
|
+
if (handle) {
|
|
189
|
+
await handle.dispose();
|
|
190
|
+
}
|
|
191
|
+
result = { resolved: true };
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case 'content': {
|
|
195
|
+
const content = await page.content();
|
|
196
|
+
result = { content };
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'title': {
|
|
200
|
+
const title = await page.title();
|
|
201
|
+
result = { title };
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'close': {
|
|
205
|
+
if (context) {
|
|
206
|
+
await context.close();
|
|
207
|
+
context = null;
|
|
208
|
+
} else if (browser) {
|
|
209
|
+
await browser.close();
|
|
210
|
+
browser = null;
|
|
211
|
+
}
|
|
212
|
+
result = { closed: true };
|
|
213
|
+
respond({ result });
|
|
214
|
+
rl.close();
|
|
215
|
+
return process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
default:
|
|
218
|
+
throw new Error(`Unknown action: ${action}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
respond({ result });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
respond({ error: { message: error.message || String(error), name: error.name } });
|
|
224
|
+
}
|
|
225
|
+
} catch (fatalError) {
|
|
226
|
+
console.error(fatalError.message || fatalError);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
rl.on('close', async () => {
|
|
232
|
+
if (context) {
|
|
233
|
+
try {
|
|
234
|
+
await context.close();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// swallow
|
|
237
|
+
}
|
|
238
|
+
} else if (browser) {
|
|
239
|
+
try {
|
|
240
|
+
await browser.close();
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// swallow
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
run().catch((error) => {
|
|
249
|
+
console.error(error.message || error);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
});
|
data/lib/camoufox/sync_api.rb
CHANGED
|
@@ -20,67 +20,228 @@ module Camoufox
|
|
|
20
20
|
|
|
21
21
|
def initialize(**kwargs)
|
|
22
22
|
@launch_options = Utils.launch_options(**kwargs).to_h
|
|
23
|
+
@pages = []
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def new_page
|
|
26
|
-
Page.new(@launch_options)
|
|
27
|
+
page = Page.new(@launch_options)
|
|
28
|
+
@pages << page
|
|
29
|
+
page.on_close { @pages.delete(page) }
|
|
30
|
+
page
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def close
|
|
30
|
-
|
|
34
|
+
@pages.each(&:close)
|
|
35
|
+
@pages.clear
|
|
31
36
|
nil
|
|
32
37
|
end
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
class Page
|
|
36
|
-
attr_reader :title, :content
|
|
37
|
-
|
|
38
41
|
def initialize(launch_options)
|
|
39
42
|
@launch_options = launch_options
|
|
43
|
+
@session = NodeRunner::Session.new(@launch_options)
|
|
40
44
|
@title = nil
|
|
41
45
|
@content = nil
|
|
46
|
+
@closed = false
|
|
47
|
+
@on_close = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def on_close(&block)
|
|
51
|
+
@on_close = block
|
|
42
52
|
end
|
|
43
53
|
|
|
44
54
|
def goto(url)
|
|
45
|
-
|
|
55
|
+
ensure_open!
|
|
56
|
+
result = @session.request('goto', 'url' => url)
|
|
46
57
|
@title = result['title']
|
|
47
58
|
@content = result['content']&.to_s
|
|
48
59
|
self
|
|
49
60
|
end
|
|
61
|
+
|
|
62
|
+
def wait_for_selector(selector, **options)
|
|
63
|
+
ensure_open!
|
|
64
|
+
raise ArgumentError, "selector must be provided" if selector.to_s.empty?
|
|
65
|
+
|
|
66
|
+
wait_options = options.compact
|
|
67
|
+
params = { 'selector' => selector }
|
|
68
|
+
params['options'] = Utils.camelize_hash(wait_options) unless wait_options.empty?
|
|
69
|
+
@session.request('wait_for_selector', params)
|
|
70
|
+
|
|
71
|
+
@title = nil
|
|
72
|
+
@content = nil
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def content
|
|
77
|
+
ensure_open!
|
|
78
|
+
@content ||= begin
|
|
79
|
+
result = @session.request('content')
|
|
80
|
+
result['content']&.to_s
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def title
|
|
85
|
+
ensure_open!
|
|
86
|
+
@title ||= begin
|
|
87
|
+
result = @session.request('title')
|
|
88
|
+
result['title']
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def close
|
|
93
|
+
return if closed?
|
|
94
|
+
|
|
95
|
+
@session.close
|
|
96
|
+
@closed = true
|
|
97
|
+
@on_close&.call(self)
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def closed?
|
|
102
|
+
@closed
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def ensure_open!
|
|
108
|
+
raise Camoufox::Error, "Page is closed" if closed?
|
|
109
|
+
end
|
|
50
110
|
end
|
|
51
111
|
|
|
52
112
|
module NodeRunner
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
113
|
+
class Session
|
|
114
|
+
def initialize(launch_options)
|
|
115
|
+
@launch_options = launch_options
|
|
116
|
+
@command_id = 0
|
|
117
|
+
@closed = false
|
|
118
|
+
spawn_session
|
|
119
|
+
wait_for_ready
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def request(action, params = {})
|
|
123
|
+
raise Camoufox::Error, "Page session is closed" if @closed
|
|
124
|
+
|
|
125
|
+
@command_id += 1
|
|
126
|
+
payload = {
|
|
127
|
+
'id' => @command_id,
|
|
128
|
+
'action' => action,
|
|
129
|
+
'params' => params,
|
|
130
|
+
}
|
|
131
|
+
write_message(payload)
|
|
132
|
+
handle_response(@command_id)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def close
|
|
136
|
+
return if @closed
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
request('close')
|
|
140
|
+
rescue NodeExecutionFailed
|
|
141
|
+
# swallow shutdown errors
|
|
142
|
+
ensure
|
|
143
|
+
@closed = true
|
|
144
|
+
cleanup
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def spawn_session
|
|
151
|
+
node_path = ::Camoufox.configuration.node_path || 'node'
|
|
152
|
+
script_path = File.expand_path('syncSession.js', __dir__)
|
|
153
|
+
env = {}
|
|
154
|
+
|
|
155
|
+
if (driver_dir = ::Camoufox.configuration.playwright_driver_dir)
|
|
156
|
+
env['NODE_PATH'] = [driver_dir, ENV['NODE_PATH']].compact.join(File::PATH_SEPARATOR)
|
|
157
|
+
env['CAMOUFOX_PLAYWRIGHT_DRIVER_DIR'] = driver_dir
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
@stdin, @stdout, stderr, @wait_thr = Open3.popen3(env, node_path, script_path)
|
|
161
|
+
@stdin.sync = true
|
|
162
|
+
@stdout.sync = true
|
|
163
|
+
|
|
164
|
+
@stderr_thread = Thread.new do
|
|
165
|
+
begin
|
|
166
|
+
stderr.each_line { |line| warn(line.chomp) }
|
|
167
|
+
rescue IOError
|
|
168
|
+
nil
|
|
169
|
+
ensure
|
|
170
|
+
stderr.close unless stderr.closed?
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
payload = Base64.strict_encode64(
|
|
175
|
+
JSON.generate(
|
|
176
|
+
options: Utils.camelize_hash(@launch_options),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
@stdin.puts(payload)
|
|
180
|
+
rescue Errno::ENOENT => e
|
|
181
|
+
raise NodeExecutionFailed.new("Failed to execute #{node_path}: #{e.message}", nil)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def wait_for_ready
|
|
185
|
+
message = read_message
|
|
186
|
+
return if message['event'] == 'ready'
|
|
187
|
+
|
|
188
|
+
raise NodeExecutionFailed.new("Invalid handshake from Playwright bridge", nil)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def handle_response(expected_id)
|
|
192
|
+
message = read_message
|
|
193
|
+
unless message['id'] == expected_id
|
|
194
|
+
raise NodeExecutionFailed.new("Mismatched response id from Playwright bridge", nil)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if (error = message['error'])
|
|
198
|
+
raise NodeExecutionFailed.new("Playwright bridge error: #{error['message']}", nil)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
message['result']
|
|
70
202
|
end
|
|
71
203
|
|
|
72
|
-
|
|
204
|
+
def write_message(payload)
|
|
205
|
+
encoded = Base64.strict_encode64(JSON.generate(payload))
|
|
206
|
+
@stdin.puts(encoded)
|
|
207
|
+
rescue IOError => e
|
|
208
|
+
raise NodeExecutionFailed.new("Failed to talk to Playwright bridge: #{e.message}", nil)
|
|
209
|
+
end
|
|
73
210
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
211
|
+
def read_message
|
|
212
|
+
line = nil
|
|
213
|
+
loop do
|
|
214
|
+
line = @stdout.gets
|
|
215
|
+
raise NodeExecutionFailed.new("Playwright bridge closed unexpectedly", nil) if line.nil?
|
|
216
|
+
|
|
217
|
+
stripped = line.strip
|
|
218
|
+
next if stripped.empty?
|
|
219
|
+
|
|
220
|
+
line = stripped
|
|
221
|
+
break
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
decoded = Base64.strict_decode64(line)
|
|
225
|
+
JSON.parse(decoded)
|
|
226
|
+
rescue ArgumentError, JSON::ParserError => e
|
|
227
|
+
raise NodeExecutionFailed.new("Invalid response from Playwright bridge: #{e.message}", nil)
|
|
228
|
+
rescue IOError => e
|
|
229
|
+
raise NodeExecutionFailed.new("Failed to read from Playwright bridge: #{e.message}", nil)
|
|
77
230
|
end
|
|
78
231
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
232
|
+
def cleanup
|
|
233
|
+
@stdin.close unless @stdin.closed?
|
|
234
|
+
@stdout.close unless @stdout.closed?
|
|
235
|
+
if @wait_thr&.alive?
|
|
236
|
+
Process.kill('TERM', @wait_thr.pid)
|
|
237
|
+
@wait_thr.join
|
|
238
|
+
else
|
|
239
|
+
@wait_thr&.value
|
|
240
|
+
end
|
|
241
|
+
@stderr_thread&.join
|
|
242
|
+
rescue Errno::ESRCH, IOError
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
84
245
|
end
|
|
85
246
|
end
|
|
86
247
|
end
|
data/lib/camoufox_native.bundle
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: camoufox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Camoufox contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-11-
|
|
10
|
+
date: 2025-11-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -81,6 +81,7 @@ files:
|
|
|
81
81
|
- README.md
|
|
82
82
|
- bin/camoufox
|
|
83
83
|
- docs/native_port.md
|
|
84
|
+
- ext/camoufox_native/Makefile
|
|
84
85
|
- ext/camoufox_native/camoufox_native.cpp
|
|
85
86
|
- ext/camoufox_native/extconf.rb
|
|
86
87
|
- lib/camoufox.rb
|
|
@@ -99,10 +100,10 @@ files:
|
|
|
99
100
|
- lib/camoufox/native_bridge.rb
|
|
100
101
|
- lib/camoufox/pkgman.rb
|
|
101
102
|
- lib/camoufox/server.rb
|
|
103
|
+
- lib/camoufox/syncSession.js
|
|
102
104
|
- lib/camoufox/sync_api.rb
|
|
103
105
|
- lib/camoufox/utils.rb
|
|
104
106
|
- lib/camoufox/virtdisplay.rb
|
|
105
|
-
- lib/camoufox/visit.js
|
|
106
107
|
- lib/camoufox/warnings.rb
|
|
107
108
|
- lib/camoufox/warnings.yml
|
|
108
109
|
- lib/camoufox/webgl/README.md
|
data/lib/camoufox/visit.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
|
|
4
|
-
function readStdinAsBase64() {
|
|
5
|
-
return new Promise((resolve, reject) => {
|
|
6
|
-
const chunks = [];
|
|
7
|
-
process.stdin.setEncoding('utf8');
|
|
8
|
-
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
9
|
-
process.stdin.on('end', () => resolve(chunks.join('')));
|
|
10
|
-
process.stdin.on('error', (error) => reject(error));
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function withDefault(value, extractor) {
|
|
15
|
-
if (!value) {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
return extractor(value);
|
|
21
|
-
} catch (error) {
|
|
22
|
-
// Surface the original failure to the caller.
|
|
23
|
-
console.warn(`camoufox: failed to initialize Playwright shim (${error.message || error})`);
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function resolvePlaywrightApi(candidate) {
|
|
29
|
-
if (!candidate) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (candidate.firefox) {
|
|
34
|
-
return candidate;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (candidate.default) {
|
|
38
|
-
const resolved = resolvePlaywrightApi(candidate.default);
|
|
39
|
-
if (resolved) {
|
|
40
|
-
return resolved;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (typeof candidate.createInProcessPlaywright === 'function') {
|
|
45
|
-
const resolved = withDefault(candidate, (mod) => mod.createInProcessPlaywright());
|
|
46
|
-
if (resolved) {
|
|
47
|
-
return resolved;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (typeof candidate.createPlaywright === 'function') {
|
|
52
|
-
const resolved = withDefault(candidate, (mod) => mod.createPlaywright({ sdkLanguage: process.env.PW_LANG_NAME || 'javascript' }));
|
|
53
|
-
if (resolved && resolved.firefox) {
|
|
54
|
-
return resolved;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (candidate.playwright) {
|
|
59
|
-
return resolvePlaywrightApi(candidate.playwright);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function loadPlaywright() {
|
|
66
|
-
const override = process.env.CAMOUFOX_PLAYWRIGHT_JS_REQUIRE;
|
|
67
|
-
if (override) {
|
|
68
|
-
return require(override);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
return require('playwright');
|
|
73
|
-
} catch (error) {
|
|
74
|
-
// fall through
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const driverDir = process.env.CAMOUFOX_PLAYWRIGHT_DRIVER_DIR;
|
|
78
|
-
if (driverDir) {
|
|
79
|
-
try {
|
|
80
|
-
return require(path.join(driverDir, 'package'));
|
|
81
|
-
} catch (error) {
|
|
82
|
-
// fall through
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
console.error('Unable to require Playwright. Install the `playwright` npm package or set CAMOUFOX_PLAYWRIGHT_JS_REQUIRE.');
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function main() {
|
|
91
|
-
const payloadB64 = await readStdinAsBase64();
|
|
92
|
-
const payload = JSON.parse(Buffer.from(payloadB64, 'base64').toString());
|
|
93
|
-
const { options, url } = payload;
|
|
94
|
-
|
|
95
|
-
if (options.executablePath && !fs.existsSync(options.executablePath)) {
|
|
96
|
-
console.warn(`camoufox: executable ${options.executablePath} not found, falling back to Playwright default`);
|
|
97
|
-
delete options.executablePath;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const playwrightModule = loadPlaywright();
|
|
101
|
-
const playwright = resolvePlaywrightApi(playwrightModule);
|
|
102
|
-
const browserType = playwright && playwright.firefox;
|
|
103
|
-
if (!browserType) {
|
|
104
|
-
console.error('Playwright module does not expose `firefox`. Provide a driver bundle or module that exports Playwright.firefox.');
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const browser = await browserType.launch(options);
|
|
109
|
-
const page = await browser.newPage();
|
|
110
|
-
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
111
|
-
try {
|
|
112
|
-
await page.waitForLoadState('networkidle', { timeout: 15000 });
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.warn(`camoufox: waitForLoadState(networkidle) warning: ${error.message || error}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const [title, content] = await Promise.all([
|
|
118
|
-
page.title(),
|
|
119
|
-
page.content(),
|
|
120
|
-
]);
|
|
121
|
-
|
|
122
|
-
console.log(JSON.stringify({ title, content }));
|
|
123
|
-
await browser.close();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
main().catch((error) => {
|
|
127
|
-
console.error(error.message || error);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
});
|