Module: Browserctl
- Defined in:
- lib/browserctl/flows/stdlib/cloudflare_solve.rb,
lib/browserctl.rb,
lib/browserctl/flow.rb,
lib/browserctl/state.rb,
lib/browserctl/client.rb,
lib/browserctl/errors.rb,
lib/browserctl/logger.rb,
lib/browserctl/policy.rb,
lib/browserctl/runner.rb,
lib/browserctl/server.rb,
lib/browserctl/session.rb,
lib/browserctl/version.rb,
lib/browserctl/redactor.rb,
lib/browserctl/workflow.rb,
lib/browserctl/constants.rb,
lib/browserctl/detectors.rb,
lib/browserctl/recording.rb,
lib/browserctl/driver/cdp.rb,
lib/browserctl/migrations.rb,
lib/browserctl/driver/base.rb,
lib/browserctl/error/codes.rb,
lib/browserctl/commands/ask.rb,
lib/browserctl/crash_report.rb,
lib/browserctl/snapshot/ref.rb,
lib/browserctl/state/bundle.rb,
lib/browserctl/commands/fill.rb,
lib/browserctl/commands/flow.rb,
lib/browserctl/commands/init.rb,
lib/browserctl/commands/page.rb,
lib/browserctl/flow_registry.rb,
lib/browserctl/commands/click.rb,
lib/browserctl/commands/state.rb,
lib/browserctl/commands/trace.rb,
lib/browserctl/format_version.rb,
lib/browserctl/replay/context.rb,
lib/browserctl/commands/cookie.rb,
lib/browserctl/commands/daemon.rb,
lib/browserctl/commands/dialog.rb,
lib/browserctl/commands/record.rb,
lib/browserctl/commands/resume.rb,
lib/browserctl/driver/cdp_page.rb,
lib/browserctl/state/transport.rb,
lib/browserctl/commands/migrate.rb,
lib/browserctl/commands/session.rb,
lib/browserctl/commands/storage.rb,
lib/browserctl/error/exit_codes.rb,
lib/browserctl/replay/telemetry.rb,
lib/browserctl/commands/snapshot.rb,
lib/browserctl/commands/workflow.rb,
lib/browserctl/workflow/promoter.rb,
lib/browserctl/snapshot/annotator.rb,
lib/browserctl/snapshot/extractor.rb,
lib/browserctl/commands/cli_output.rb,
lib/browserctl/commands/screenshot.rb,
lib/browserctl/server/idle_watcher.rb,
lib/browserctl/server/page_session.rb,
lib/browserctl/snapshot/serializer.rb,
lib/browserctl/state/transports/s3.rb,
lib/browserctl/replay/snapshot_diff.rb,
lib/browserctl/secret_resolvers/env.rb,
lib/browserctl/server/handlers/hitl.rb,
lib/browserctl/snapshot/fingerprint.rb,
lib/browserctl/flows/stdlib/totp_2fa.rb,
lib/browserctl/secret_resolvers/base.rb,
lib/browserctl/server/handlers/state.rb,
lib/browserctl/state/transports/file.rb,
lib/browserctl/workflow/flow_wrapper.rb,
lib/browserctl/detectors/auth_required.rb,
lib/browserctl/error/suggested_actions.rb,
lib/browserctl/server/handlers/cookies.rb,
lib/browserctl/server/handlers/session.rb,
lib/browserctl/server/handlers/storage.rb,
lib/browserctl/server/snapshot_builder.rb,
lib/browserctl/secret_resolver_registry.rb,
lib/browserctl/server/handlers/devtools.rb,
lib/browserctl/server/command_dispatcher.rb,
lib/browserctl/workflow/promotion_ledger.rb,
lib/browserctl/replay/fingerprint_matcher.rb,
lib/browserctl/server/handlers/navigation.rb,
lib/browserctl/server/handlers/interaction.rb,
lib/browserctl/server/handlers/observation.rb,
lib/browserctl/secret_resolvers/one_password.rb,
lib/browserctl/server/handlers/error_payload.rb,
lib/browserctl/state/transports/one_password.rb,
lib/browserctl/server/handlers/daemon_control.rb,
lib/browserctl/server/handlers/page_lifecycle.rb,
lib/browserctl/secret_resolvers/macos_keychain.rb
Overview
Pauses for a human to solve a Cloudflare challenge (Turnstile, “Just a moment…”, interactive checkbox), then verifies the challenge cleared before returning. Optionally saves the post-solve session under a name you can reload later with ‘state load` or `session_load`.
Reuses Browserctl::Detectors.cloudflare? — the server-side detector already shipped in v0.8 — by adapting the client-facing PageProxy to the duck-typed (current_url, body) interface the detector expects.
Defined Under Namespace
Modules: Commands, CrashReport, Detectors, Driver, Flows, FormatVersion, Migrations, Policy, Replay, SecretResolvers, Snapshot, State, Workflow
Classes: AuthRequiredError, BrowserNotFound, Client, CommandDispatcher, DaemonUnavailableError, DomainNotAllowed, Error, Flow, FlowConditionDef, FlowContext, FlowError, FlowParamDef, FlowParamError, FlowPostconditionError, FlowPreconditionError, FlowRegistry, FlowStepDef, FlowStepError, HeaderlessLogDevice, IdleWatcher, JsonlFormatter, KeyNotFound, MultiLogger, PageNotFound, PageProxy, PageSession, ParamDef, PathNotAllowed, ProtocolMismatch, Recording, Redactor, RefNotFound, Runner, SecretResolverError, SecretResolverRegistry, SelectorNotFound, Server, Session, SnapshotBuilder, StepDef, StepResult, TimeoutError, WorkflowContext, WorkflowDefinition, WorkflowError
Constant Summary
collapse
- LEVEL_MAP =
{
"debug" => ::Logger::DEBUG,
"info" => ::Logger::INFO,
"warn" => ::Logger::WARN,
"error" => ::Logger::ERROR
}.freeze
- LOG_SHIFT_AGE =
JSONL rotation policy. Stdlib ‘Logger` rotates by size when given an integer `shift_age` and `shift_size`.
10
- LOG_SHIFT_SIZE =
keep last 10 rotated files
10 * 1024 * 1024
- VERSION =
"0.12.0"
- WORKFLOW_FORMAT_VERSION =
Workflow-file format version. Workflows are Ruby files; the schema gate is a top-of-file comment header:
Unlike bundles and recordings, an unsupported or missing version on a workflow file is a warning, not a hard failure. Workflows are human-authored Ruby — the loader prefers to surface drift via stderr and let the file run, rather than block execution. See docs/reference/format-versions.md.
1
- SUPPORTED_WORKFLOW_FORMAT_VERSIONS =
[WORKFLOW_FORMAT_VERSION].freeze
/^\s*#\s*format_version:\s*(\d+)\s*$/
- BROWSERCTL_DIR =
File.expand_path("~/.browserctl")
- IDLE_TTL =
30 * 60
- PROTOCOL_VERSION =
Increment when a breaking wire protocol change ships (new field names, removed commands, changed response shapes). Clients read this from ‘ping` to verify compatibility before sending commands.
"3"
- SOCKET_PATH =
Backward-compatible constants
socket_path
- PID_PATH =
pid_path
Class Method Summary
collapse
Class Method Details
.all_daemon_names ⇒ Object
38
39
40
41
|
# File 'lib/browserctl/constants.rb', line 38
def self.all_daemon_names
all_daemon_sockets.map { |f| File.basename(f, ".sock") }
.map { |n| n == "browserd" ? nil : n }
end
|
.all_daemon_sockets ⇒ Object
34
35
36
|
# File 'lib/browserctl/constants.rb', line 34
def self.all_daemon_sockets
Dir[File.join(BROWSERCTL_DIR, "*.sock")]
end
|
.build_jsonl_logger(level, component) ⇒ Object
125
126
127
128
129
130
131
132
133
134
135
136
137
|
# File 'lib/browserctl/logger.rb', line 125
def self.build_jsonl_logger(level, component)
dir = log_dir
FileUtils.mkdir_p(dir, mode: 0o700)
path = File.join(dir, "#{component}.log")
device = HeaderlessLogDevice.new(path, shift_age: LOG_SHIFT_AGE, shift_size: LOG_SHIFT_SIZE)
log = ::Logger.new(device)
log.level = level
log.progname = component
log.formatter = JsonlFormatter.new(component: component)
log
rescue StandardError
nil
end
|
.build_logger(level_name, log_path: nil, component: "daemon", jsonl: true) ⇒ Object
Build a logger that writes:
- human-readable lines to stderr (unchanged behaviour)
- human-readable lines to log_path: when given (the daemon tail file)
- structured JSONL lines to ~/.browserctl/logs/<component>.log (rotating
10 files x 10MB) when jsonl: is true
JSONL output is purely additive — existing stderr/stdout behaviour is preserved so scripted callers see no change.
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
# File 'lib/browserctl/logger.rb', line 92
def self.build_logger(level_name, log_path: nil, component: "daemon", jsonl: true)
level = LEVEL_MAP.fetch(level_name.to_s.downcase, ::Logger::INFO)
text_formatter = proc do |sev, t, prog, msg|
"#{t.strftime('%Y-%m-%dT%H:%M:%S')} #{sev[0]} [#{prog}] #{format_text_msg(msg)}\n"
end
loggers = [make_logger($stderr, level, text_formatter)]
if log_path
FileUtils.mkdir_p(File.dirname(log_path), mode: 0o700)
FileUtils.touch(log_path)
File.chmod(0o600, log_path)
loggers << make_logger(log_path, level, text_formatter)
end
if jsonl
jsonl_logger = build_jsonl_logger(level, component)
loggers << jsonl_logger if jsonl_logger
end
loggers.length == 1 ? loggers.first : MultiLogger.new(*loggers)
end
|
.flow(name, &block) ⇒ Object
192
193
194
195
196
197
198
|
# File 'lib/browserctl/flow.rb', line 192
def self.flow(name, &block)
raise ArgumentError, "Browserctl.flow requires a block" unless block
flow = Flow.new(name).tap { |f| f.instance_exec(&block) }
register_flow(flow)
flow
end
|
.flow_registry_reset! ⇒ Object
212
213
214
|
# File 'lib/browserctl/flow.rb', line 212
def self.flow_registry_reset!
@flow_registry_mutex.synchronize { @flow_registry.clear }
end
|
.flow_registry_snapshot ⇒ Object
208
209
210
|
# File 'lib/browserctl/flow.rb', line 208
def self.flow_registry_snapshot
@flow_registry_mutex.synchronize { @flow_registry.dup }
end
|
.log_dir ⇒ Object
Resolved at call time so tests can override BROWSERCTL_DIR via stub_const.
22
23
24
|
# File 'lib/browserctl/logger.rb', line 22
def self.log_dir
File.join(BROWSERCTL_DIR, "logs")
end
|
.log_path(name = nil) ⇒ Object
20
21
22
|
# File 'lib/browserctl/constants.rb', line 20
def self.log_path(name = nil)
File.join(BROWSERCTL_DIR, name ? "#{name}.log" : "browserd.log")
end
|
.logger ⇒ Object
76
77
78
|
# File 'lib/browserctl/logger.rb', line 76
def self.logger
@logger ||= build_logger("info")
end
|
.logger=(instance) ⇒ Object
80
81
82
|
# File 'lib/browserctl/logger.rb', line 80
def self.logger=(instance)
@logger = instance
end
|
.lookup_flow(name) ⇒ Object
204
205
206
|
# File 'lib/browserctl/flow.rb', line 204
def self.lookup_flow(name)
@flow_registry_mutex.synchronize { @flow_registry[name.to_s] }
end
|
.lookup_plugin_command(name) ⇒ Object
19
20
21
|
# File 'lib/browserctl.rb', line 19
def self.lookup_plugin_command(name)
@plugin_commands_mutex.synchronize { @plugin_commands[name.to_s] }
end
|
.lookup_workflow(name) ⇒ Object
552
553
554
|
# File 'lib/browserctl/workflow.rb', line 552
def self.lookup_workflow(name)
@registry_mutex.synchronize { @registry[name.to_s] }
end
|
.next_daemon_name ⇒ Object
Returns nil when the default (unnamed) slot is free; otherwise returns “d1”, “d2”, etc.
25
26
27
28
29
30
31
32
|
# File 'lib/browserctl/constants.rb', line 25
def self.next_daemon_name
return nil unless File.exist?(socket_path)
1.upto(99) do |i|
return "d#{i}" unless File.exist?(socket_path("d#{i}"))
end
raise Browserctl::Error, "too many running daemons (limit: 99)"
end
|
Parses the ‘# format_version: N` header from a workflow file’s source. Scans only the contiguous leading comment block (and blank lines) so the header cannot be smuggled in mid-file. Returns the integer if present, or nil if the file has no version header.
37
38
39
40
41
42
43
44
45
46
47
48
|
# File 'lib/browserctl/workflow.rb', line 37
def self.parse_workflow_format_version(source)
source.each_line do |line|
stripped = line.strip
next if stripped.empty?
break unless stripped.start_with?("#")
if (m = line.match(WORKFLOW_FORMAT_VERSION_HEADER))
return Integer(m[1])
end
end
nil
end
|
.pid_path(name = nil) ⇒ Object
16
17
18
|
# File 'lib/browserctl/constants.rb', line 16
def self.pid_path(name = nil)
File.join(BROWSERCTL_DIR, name ? "#{name}.pid" : "browserd.pid")
end
|
.plugin_commands_snapshot ⇒ Object
23
24
25
|
# File 'lib/browserctl.rb', line 23
def self.plugin_commands_snapshot
@plugin_commands_mutex.synchronize { @plugin_commands.dup }
end
|
.register_command(name, &block) ⇒ Object
15
16
17
|
# File 'lib/browserctl.rb', line 15
def self.register_command(name, &block)
@plugin_commands_mutex.synchronize { @plugin_commands[name.to_s] = block }
end
|
.register_flow(flow) ⇒ Object
200
201
202
|
# File 'lib/browserctl/flow.rb', line 200
def self.register_flow(flow)
@flow_registry_mutex.synchronize { @flow_registry[flow.name] = flow }
end
|
.registry_snapshot ⇒ Object
556
557
558
|
# File 'lib/browserctl/workflow.rb', line 556
def self.registry_snapshot
@registry_mutex.synchronize { @registry.dup }
end
|
.socket_path(name = nil) ⇒ Object
12
13
14
|
# File 'lib/browserctl/constants.rb', line 12
def self.socket_path(name = nil)
File.join(BROWSERCTL_DIR, name ? "#{name}.sock" : "browserd.sock")
end
|
Reads a workflow file and warns to stderr when the ‘format_version:` header is missing or declares an unsupported version. Always returns the parsed integer (or nil) — never raises. Callers should still `load` the file regardless.
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
# File 'lib/browserctl/workflow.rb', line 54
def self.verify_workflow_format_version!(path)
source = File.read(path)
version = parse_workflow_format_version(source)
if version.nil?
warn "[browserctl] workflow #{path} is missing a `# format_version: N` header " \
"(expected #{WORKFLOW_FORMAT_VERSION}); proceeding anyway"
elsif !SUPPORTED_WORKFLOW_FORMAT_VERSIONS.include?(version)
warn "[browserctl] workflow #{path} format_version=#{version} is not supported " \
"(expected #{WORKFLOW_FORMAT_VERSION}); proceeding anyway"
end
version
end
|
.workflow(name) ⇒ Object
546
547
548
549
550
|
# File 'lib/browserctl/workflow.rb', line 546
def self.workflow(name, &)
defn = WorkflowDefinition.new(name.to_s)
defn.instance_exec(&)
@registry_mutex.synchronize { @registry[name.to_s] = defn }
end
|