Class: AdsPowerClient

Inherits:
Object
  • Object
show all
Defined in:
lib/adspower-client.rb

Constant Summary collapse

CLOUD_API_BASE =
'https://api.adspower.com/v1'
COUNTRY_LANG =

Constante generada en tiempo de ejecución:

ISO3166::Country.all.each_with_object({}) do |country, h|
    # El primer idioma oficial (ISO 639-1) que encuentre:
    language_code = country.languages&.first || 'en'
    # Construimos la etiqueta BCP47 Language-Region:
    h[country.alpha2] = "#{language_code}-#{country.alpha2}"
end.freeze
LOCK_FILE =
'/tmp/adspower_api_lock'
@@drivers =

control over the drivers created, in order to not create the same driver twice and not generate memory leaks. reference: github.com/leandrosardi/adspower-client/issues/4

{}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(h = {}) ⇒ AdsPowerClient

Returns a new instance of AdsPowerClient.



31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/adspower-client.rb', line 31

def initialize(h={})
    self.key = h[:key] # mandatory
    self.port = h[:port] || '50325'
    self.server_log = h[:server_log] || '~/adspower-client.log'
    self.adspower_listener = h[:adspower_listener] || 'http://127.0.0.1'
    
    # DEPRECATED
    self.adspower_default_browser_version = h[:adspower_default_browser_version] || '116'

    # PENDING
    self.cloud_token = h[:cloud_token]
end

Instance Attribute Details

#adspower_default_browser_versionObject



23
24
25
# File 'lib/adspower-client.rb', line 23

def adspower_default_browser_version
  @adspower_default_browser_version
end

#adspower_listenerObject



23
24
25
# File 'lib/adspower-client.rb', line 23

def adspower_listener
  @adspower_listener
end

#cloud_tokenObject



23
24
25
# File 'lib/adspower-client.rb', line 23

def cloud_token
  @cloud_token
end

#keyObject



23
24
25
# File 'lib/adspower-client.rb', line 23

def key
  @key
end

#portObject



23
24
25
# File 'lib/adspower-client.rb', line 23

def port
  @port
end

#server_logObject



23
24
25
# File 'lib/adspower-client.rb', line 23

def server_log
  @server_log
end

Class Method Details

.cleanup(id) ⇒ Object

Quit and remove a specific driver (safe)



117
118
119
120
121
122
123
124
125
126
# File 'lib/adspower-client.rb', line 117

def self.cleanup(id)
    drv = @@drivers[id]
    begin
        drv.quit if drv
    rescue => e
        # best-effort: ignore but log if desired
    ensure
        @@drivers.delete(id)
    end
end

.cleanup_allObject

Quit and remove all drivers (safe)



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/adspower-client.rb', line 103

def self.cleanup_all
    @@drivers.keys.each do |id|
        drv = @@drivers[id]
        begin
            drv.quit if drv
        rescue => e
        # best-effort: ignore but log if desired
        ensure
            @@drivers.delete(id)
        end
    end
end

.driversObject



98
99
100
# File 'lib/adspower-client.rb', line 98

def self.drivers
    @@drivers
end

Instance Method Details

#acquire_lockObject

Acquire the lock



45
46
47
48
# File 'lib/adspower-client.rb', line 45

def acquire_lock
    @lockfile ||= File.open(LOCK_FILE, File::CREAT | File::RDWR)
    @lockfile.flock(File::LOCK_EX)
end

#check(id) ⇒ Object

Check if the browser session for the given user profile is active.



517
518
519
520
521
522
523
524
525
# File 'lib/adspower-client.rb', line 517

def check(id)
    with_lock do
        url = "#{self.adspower_listener}:#{port}/api/v1/browser/active?user_id=#{id}"
        uri = URI.parse(url)
        res = Net::HTTP.get(uri)
        return false if JSON.parse(res)['msg'] != 'success'
        JSON.parse(res)['data']['status'] == 'Active'
    end
end

#cloud_profile_quotaObject

Return a hash with:

• :limit     ⇒ total profile slots allowed (-1 = unlimited)
• :used      ⇒ number of profiles currently created
• :remaining ⇒ slots left (nil if unlimited)

Fetch your real profile quota from the Cloud API



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/adspower-client.rb', line 240

def cloud_profile_quota
    uri = URI("#{CLOUD_API_BASE}/account/get_info")
    req = Net::HTTP::Get.new(uri)
    req['Authorization'] = "Bearer #{self.cloud_token}"

    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
        http.request(req)
    end
    data = JSON.parse(res.body)
    raise "Cloud API error: #{data['msg']}" unless data['code'] == 0

    allowed = data['data']['total_profiles_allowed'].to_i
    used    = data['data']['profiles_used'].to_i
    remaining = allowed < 0 ? nil : (allowed - used)

    { limit:     allowed,
    used:      used,
    remaining: remaining }
end

#createObject

Create a new user profile via API call and return the ID of the created user.



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/adspower-client.rb', line 261

def create
    with_lock do
        url = "#{self.adspower_listener}:#{port}/api/v1/user/create"
        body = {
            'group_id' => '0',
            'proxyid' => '1',
            'fingerprint_config' => {
                'browser_kernel_config' => {"version": self.adspower_default_browser_version, "type": "chrome"}
            }
        }
        # API call
        res = BlackStack::Netting.call_post(url, body)
        ret = JSON.parse(res.body)
        raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
        ret['data']['id']
    end
end

#create2(name:, proxy_config:, group_id: '0', browser_version: nil, os: 'linux64', fingerprint: nil, platform: '', tabs: [], username: '', password: '', fakey: '', cookie: nil) ⇒ Object

Create a new desktop profile with:

• name, proxy, fingerprint, etc (unchanged)
• platform (e.g. "linkedin.com")
• tabs     (Array of URLs to open)
• username / password / fakey for that platform

Parameters:

  • name (String)

    the profile’s display name

  • proxy_config (Hash)

    keys: :ip, :port, :user, :password, :proxy_soft (default ‘other’), :proxy_type (default ‘http’)

  • group_id (String) (defaults to: '0')

    which AdsPower group to assign (default ‘0’)

  • browser_version (String) (defaults to: nil)

    optional Chrome version to use (must match Chromedriver). Only applies if ‘fingerprint` is nil, as custom fingerprints override kernel settings.

  • os (String) (defaults to: 'linux64')

    target OS for Chrome binary (one of ‘linux64’, ‘mac-x64’, ‘mac-arm64’, ‘win32’, ‘win64’; default ‘linux64’); used to filter the known-good versions JSON so we pick a build that actually ships for that platform

  • fingerprint (Hash, nil) (defaults to: nil)

    optional fingerprint configuration. If not provided, a stealth-ready default is applied with DNS-over-HTTPS, spoofed WebGL/Canvas/audio, consistent User-Agent and locale, and hardening flags to minimize detection risks from tools like BrowserScan, Cloudflare, and Arkose Labs.

  • platform (String) (defaults to: '')

    (optional) target site domain, e.g. ‘linkedin.com’

  • tabs (Array<String>) (defaults to: [])

    (optional) array of URLs to open on launch

  • username (String) (defaults to: '')

    (optional) platform login username

  • password (String) (defaults to: '')

    (optional) platform login password

  • fakey (String, nil) (defaults to: '')

    optional 2FA key

Returns:

  • String the new profile’s ID



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/adspower-client.rb', line 314

def create2(
    name:, 
    proxy_config:, 
    group_id: '0', 
    browser_version: nil,
    os:              'linux64', # new: one of linux64, mac-x64, mac-arm64, win32, win64
    fingerprint:     nil,
    platform:        '',        # default: no platform
    tabs:            [],        # default: no tabs to open
    username:        '',        # default: no login
    password:        '',        # default: no password
    fakey:           '',        # leave blank if no 2FA
    cookie:          nil        # import cookies - Type: Text - Format: JSON - username/password are ignored if cookie is not nil.
)
    browser_version ||= adspower_default_browser_version
    
    # 0) Resolve full Chrome version ─────────────────────────────
    # Fetch the list of known-good Chrome versions and pick the highest
    uri = URI('https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json')
    resp = Net::HTTP.get_response(uri)
    unless resp.is_a?(Net::HTTPSuccess)
        raise "Error fetching Chrome versions: HTTP #{resp.code}"
    end
    listing = JSON.parse(resp.body)
    entries = listing['versions'] || []
    # keep only those entries whose version matches prefix *and* has a download for our OS
    matches = entries.
      select { |e|
        e['version'].start_with?("#{browser_version}.") &&
        e.dig('downloads','chrome').any? { |d| d['platform'] == os }
      }.
      map { |e| e['version'] }
    if matches.empty?
        raise "Chrome version '#{browser_version}' not found in known-good versions list"
    end
    # pick the highest patch/build by semantic compare
    full_version = matches
        .map { |ver| ver.split('.').map(&:to_i) }
        .max
        .join('.')

    # 1) Hacemos GeoIP sobre la IP del proxy
    geo = geolocate(proxy_config[:ip])
    lang = COUNTRY_LANG[geo[:country_code]] || "en-US"
    screen_res = "1920_1080"

    with_lock do
        url = "#{adspower_listener}:#{port}/api/v2/browser-profile/create"
        body = {
            # ─── GENERAL & PROXY ─────────────────────────────
            'name'            => name,
            'group_id'        => group_id,
            'user_proxy_config' => {
                'proxy_soft'     => proxy_config[:proxy_soft]     || 'other',
                'proxy_type'     => proxy_config[:proxy_type]     || 'socks5',
                'proxy_host'     => proxy_config[:ip],
                'proxy_port'     => proxy_config[:port].to_s,
                'proxy_user'     => proxy_config[:user],
                'proxy_password' => proxy_config[:password],

                # ─── FORCE ALL DNS THROUGH PROXY ─────────────────
                # Avoid DNS-Leak
                "proxy_dns":        1,                           # 1 = yes, 0 = no
                "dns_servers":     ["8.8.8.8","8.8.4.4"]         # optional: your choice of DNS
            },

            # ─── PLATFORM ─────────────────────────────────────
            'platform'          => platform,  # must be one of AdsPower’s supported “sites”
            'tabs'              => tabs,      # array of URLs to open
            'username'          => username,
            'password'          => password,
            'fakey'             => fakey,     # 2FA, if any
            'cookie'            => cookie,

            # ─── FINGERPRINT ──────────────────────────────────
            "fingerprint_config" => fingerprint || {

                # ─── 0) DNS Leak Prevention ───────────────────────────
                # Even with “proxy_dns” forced on, a few ISPs will still 
                # silently intercept every UDP:53 out of your AdsPower VPS 
                # and shove it into their own resolver farm (the classic 
                # “transparent DNS proxy” attack that BrowserScan is warning you about). 
                #
                # Because you refuse to hot-patch your Chrome via extra args or CDP, 
                # the only way to survive an ISP-level hijack is to push all name lookups 
                # into an encrypted channel that the ISP simply can’t touch: DNS-over-HTTPS (DoH).
                #
                # Here’s the minimal change you need to bake into your AdsPower profile at 
                # creation time so that every DNS query happens inside Chrome’s DoH stack:
                # 
                "extra_launch_flags" => [
                    # === DNS over HTTPS only ===
                    "--enable-features=DnsOverHttps",
                    "--dns-over-https-mode=secure",
                    "--dns-over-https-templates=https://cloudflare-dns.com/dns-query",
                    "--disable-ipv6",

                    # === hide “Chrome is being controlled…” banner ===
                    #
                    # Even though you baked in the DoH flags under extra_launch_flags, 
                    # you never told Chrome to hide its “automation” banners or black-hole 
                    # all other DNS lookups — and BrowserScan still sees those UDP:53 calls 
                    # leaking out.
                    # 
                    # What you need is to push three more flags into your profile creation, 
                    # and then attach with the exact same flags when Selenium hooks in.
                    # 
                    "--disable-blink-features=AutomationControlled",
                    "--disable-infobars",
                    "--disable-features=TranslateUI",            # optional but reduces tell-tale infobars
                    "--host-resolver-rules=MAP * 0.0.0.0,EXCLUDE localhost,EXCLUDE cloudflare-dns.com"
                ],

                # ─── 1) Kernel & versión ───────────────────────────
                "browser_kernel_config" => {
                    "version" => browser_version,   # aquí usamos el parámetro
                    "type"    => "chrome"
                },

                # ─── 2) Timezone & locale ──────────────────────────
                "automatic_timezone" => "1",
                #"timezone"           => geo[:time_zone],
                "language"           => [ lang ],

                # ─── 3) User-Agent coherente ───────────────────────
                "ua_category" => "desktop",
                'ua' => "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/#{full_version} Safari/537.36",
                "is_mobile"   => false,

                # ─── 4) Pantalla y plataforma ──────────────────────
                # It turns out that “Based on User-Agent” is purely a UI setting
                #"screen_resolution" => screen_res, "1920_1080"
                "platform"          => "Linux x86_64",

                # ─── 5) Canvas & WebGL custom ─────────────────────
                "canvas"        => "1",
                "webgl_image"   => "1",
                "webgl"         => "0",    # 0=deshabilitado, 2=modo custom, 3=modo random-match
                "webgl_config"  => {
                    "unmasked_vendor"   => "Intel Inc.",
                    "unmasked_renderer" => "ANGLE (Intel, Mesa Intel(R) Xe Graphics (TGL GT2), OpenGL 4.6)",
                    "webgpu"            => { "webgpu_switch" => "1" }
                },

                # ─── 6) Resto de ajustes ───────────────────────────
                "webrtc"   => "disabled",   # WebRTC sí admite “disabled”
                "flash"    => "block",      # Flash únicamente “allow” o “block”
                "fonts"    => []            # usar fonts por defecto
            }
        }

        res = BlackStack::Netting.call_post(url, body)
        ret = JSON.parse(res.body)
        raise "Error creating profile: #{ret['msg']}" unless ret['code'] == 0

        ret['data']['profile_id']
    end
end

#delete(id) ⇒ Object

Delete a user profile via API call.



474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/adspower-client.rb', line 474

def delete(id)
    with_lock do
        url = "#{self.adspower_listener}:#{port}/api/v1/user/delete"
        body = {
            'api_key' => self.key,
            'user_ids' => [id],
        }
        # API call
        res = BlackStack::Netting.call_post(url, body)
        ret = JSON.parse(res.body)
        raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
    end
end

#driver(id, headless = false) ⇒ Object

Attach to the existing browser session with Selenium WebDriver.



528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/adspower-client.rb', line 528

def driver(id, headless=false)
    # Return the existing driver if it's still active.
    old = @@drivers[id]
    return old if old

    # Otherwise, start the driver
    ret = self.start(id, headless)

    # Attach test execution to the existing browser
    url = ret['data']['ws']['selenium']
    opts = Selenium::WebDriver::Chrome::Options.new
    opts.add_option("debuggerAddress", url)

    # Connect to the existing browser
    driver = Selenium::WebDriver.for(:chrome, options: opts)

    # Save the driver
    @@drivers[id] = driver

    # Return the driver
    driver
end

#driver2(id, headless: false, read_timeout: 180) ⇒ Object



551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/adspower-client.rb', line 551

def driver2(id, headless: false, read_timeout: 180)
    #return @@drivers[id] if @@drivers[id]
    # If we have a cached driver, verify it's still valid
    if @@drivers[id]
        begin
            # quick, non-destructive sanity check: ask for window_handles
            @@drivers[id].window_handles
            return @@drivers[id]
        rescue  Selenium::WebDriver::Error::InvalidSessionIdError,
                Selenium::WebDriver::Error::NoSuchWindowError,
                Errno::ECONNREFUSED => e
            # stale/broken driver: best-effort cleanup and continue to create a new one
            #warn "detected stale driver for #{id}: #{e.class}: #{e.message}"
            self.class.cleanup(id)
        rescue => e
            #warn "unexpected error checking cached driver: #{e.class}: #{e.message}"
            self.class.cleanup(id)
        end
    end

    # 1) start the AdsPower profile / grab its WebSocket URL
    data = start(id, headless)['data']
    ws   = data['ws']['selenium']  # e.g. "127.0.0.1:XXXXX"
  
    # 2) attach with DevTools (no more excludeSwitches or caps!)
    opts = Selenium::WebDriver::Chrome::Options.new
    opts.debugger_address = ws
    opts.add_argument('--headless') if headless
  
    http = Selenium::WebDriver::Remote::Http::Default.new
    http.read_timeout = read_timeout
  
    driver = Selenium::WebDriver.for(:chrome, options: opts, http_client: http)

    driver.execute_cdp(
        'Page.addScriptToEvaluateOnNewDocument',
        source: <<~JS
            // 1) remove any leftover cdc_… / webdriver hooks
            for (const k of Object.getOwnPropertyNames(window)) {
            if (k.startsWith('cdc_') || k.includes('webdriver')) {
                try { delete window[k]; } catch(e){}
            }
            }

            // 2) stub out window.chrome so Chrome-based detection thinks this is “normal” Chrome
            window.chrome = { runtime: {} };
        JS
    )

    @@drivers[id] = driver
    driver
end

#find_group_id_by_name(name) ⇒ Object

Find the numeric ID for a given group name (exact match)



187
188
189
190
# File 'lib/adspower-client.rb', line 187

def find_group_id_by_name(name)
    grp = list_groups.find { |g| g['group_name'].casecmp(name).zero? }
    grp ? grp['group_id'] : nil
end

#geolocate(ip) ⇒ Object

Lookup GeoIP gratuito (freegeoip.app) y parseo básico



281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/adspower-client.rb', line 281

def geolocate(ip)
    uri = URI("https://freegeoip.app/json/#{ip}")
    res = Net::HTTP.get(uri)
    h = JSON.parse(res)
    {
    country_code: h["country_code"],
    time_zone:    h["time_zone"],
    latitude:     h["latitude"],
    longitude:    h["longitude"]
    }
rescue
    # Fallback genérico
    { country_code: "US", time_zone: "America/New_York", latitude: 38.9, longitude: -77.0 }
end

#html(url) ⇒ Object

DEPRECATED - Use Zyte instead of this method.

Create a new profile, start the browser, visit a page, grab the HTML, and clean up.



610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
# File 'lib/adspower-client.rb', line 610

def html(url)
    ret = {
        :profile_id => nil,
        :html => nil,
        :status => 'success',
    }
    id = nil
    html = nil
    begin
        # Create the profile
        sleep(1)
        id = self.create

        # Update the result
        ret[:profile_id] = id

        # Start the profile and attach the driver
        driver = self.driver(id)

        # Get HTML
        driver.get(url)
        html = driver.page_source

        # Update the result
        ret[:html] = html

        # Stop the profile
        sleep(1)
        driver.quit
        self.stop(id)

        # Delete the profile
        sleep(1)
        self.delete(id)

        # Reset ID
        id = nil
    rescue => e
        # Stop and delete current profile if an error occurs
        if id
            sleep(1)
            self.stop(id)
            sleep(1)
            driver.quit if driver
            self.delete(id) if id
        end
        # Inform the exception
        ret[:status] = e.to_s
#        # process interruption
#        rescue SignalException, SystemExit, Interrupt => e 
#            if id
#                sleep(1) # Avoid the "Too many request per second" error
#                self.stop(id)
#                sleep(1) # Avoid the "Too many request per second" error
#                driver.quit
#                self.delete(id) if id
#            end # if id
    end
    # Return
    ret
end

#list_groupsObject

Fetch all groups



178
179
180
181
182
183
184
# File 'lib/adspower-client.rb', line 178

def list_groups
    uri  = URI("#{adspower_listener}:#{port}/api/v1/group/list?api_key=#{key}")
    resp = Net::HTTP.get(uri)
    data = JSON.parse(resp)
    raise "Error listing groups: #{data['msg']}" unless data['code'] == 0
    data['data']['list']   # => [{ "id" => 0, "name" => "Ungrouped" }, …]
end

#list_profiles(group_id: nil) ⇒ Object

returns an Array of profile‑hashes from the local API



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/adspower-client.rb', line 193

def list_profiles(group_id: nil)
    all = []
    page = 1

    loop do
        params = { page: page, limit: 100 }
        params[:group_id] = group_id if group_id
        url    = "#{adspower_listener}:#{port}/api/v2/browser-profile/list"
        resp   = BlackStack::Netting.call_post(url, params)
        data   = JSON.parse(resp.body)
        break unless data['code'] == 0 && data.dig('data','list').any?
        batch = data['data']['list']
        all += batch
        page += 1
    end

    all
end

#online?Boolean

Send a GET request to “#url/status” and return true if it responded successfully.

Returns:

  • (Boolean)


164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/adspower-client.rb', line 164

def online?
    with_lock do
        begin
            url = "#{self.adspower_listener}:#{port}/status"
            uri = URI.parse(url)
            res = Net::HTTP.get(uri)
            return JSON.parse(res)['msg'] == 'success'
        rescue => e
            return false
        end
    end
end

#profile_count(group_id: nil) ⇒ Object

Count current profiles (optionally filtered by group)



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/adspower-client.rb', line 213

def profile_count(group_id: nil)
    count = 0
    page  = 1

    loop do
    params = { page: page, limit: 100 }
    params[:group_id] = group_id if group_id
    url   = "#{adspower_listener}:#{port}/api/v2/browser-profile/list"
    res   = BlackStack::Netting.call_post(url, params)
    data  = JSON.parse(res.body)
    raise "Error listing profiles: #{data['msg']}" unless data['code'] == 0

    list = data['data']['list']
    count += list.size
    break if list.size < 100

    page += 1
    end

    count
end

#release_lockObject

Release the lock



51
52
53
54
55
56
57
# File 'lib/adspower-client.rb', line 51

def release_lock
    if @lockfile
        @lockfile.flock(File::LOCK_UN)
        # don't close while you expect further operations; close only on object shutdown
        # @lockfile.close
    end
end

#server_pidsObject

Return an array of PIDs of all the adspower_global processes running on the local computer.



78
79
80
# File 'lib/adspower-client.rb', line 78

def server_pids
    `ps aux | grep "adspower_global" | grep -v grep | awk '{print $2}'`.split("\n")
end

#server_start(timeout = 30) ⇒ Object

Run async command to start AdsPower server in headless mode. Wait up to 10 seconds to start the server, or raise an exception.



84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/adspower-client.rb', line 84

def server_start(timeout=30)
    `xvfb-run --auto-servernum --server-args='-screen 0 1024x768x24' /usr/bin/adspower_global --headless=true --api-key=#{self.key.to_s} --api-port=#{self.port.to_s} > #{self.server_log} 2>&1 &`
    # wait up to 10 seconds to start the server
    timeout.times do
        break if self.online?
        sleep(1)
    end
    # add a delay of 5 more seconds
    sleep(5)
    # raise an exception if the server is not running
    raise "Error: the server is not running" if self.online? == false
    return
end

#server_stopObject

Kill all the adspower_global processes running on the local computer.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/adspower-client.rb', line 144

def server_stop
    with_lock do
        self.server_pids.each { |pid|
            `kill -9 #{pid}`
        }
        # clean up @@driver
        @@drivers.each do |id, driver|
            begin
                driver.quit if driver
            rescue => e
                # Log or handle the exception if needed
            ensure
                @@drivers[id] = nil
            end
        end
    end
    return
end

#start(id, headless = false) ⇒ Object

Start the browser with the given user profile and return the connection details.



489
490
491
492
493
494
495
496
497
498
# File 'lib/adspower-client.rb', line 489

def start(id, headless=false)
    with_lock do
        url = "#{self.adspower_listener}:#{port}/api/v1/browser/start?user_id=#{id}&headless=#{headless ? '1' : '0'}"
        uri = URI.parse(url)
        res = Net::HTTP.get(uri)
        ret = JSON.parse(res)
        raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
        ret
    end
end

#stop(id) ⇒ Object

Stop the browser session for the given user profile.



501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'lib/adspower-client.rb', line 501

def stop(id)
    with_lock do
        if @@drivers[id] && self.check(id)
            @@drivers[id].quit
            @@drivers[id] = nil
        end

        uri = URI.parse("#{self.adspower_listener}:#{port}/api/v1/browser/stop?user_id=#{id}")
        res = Net::HTTP.get(uri)
        ret = JSON.parse(res)
        raise "Error: #{ret.to_s}" if ret['msg'].to_s.downcase != 'success'
        ret
    end
end

#with_lockObject

Wrapper method for critical sections



70
71
72
73
74
75
# File 'lib/adspower-client.rb', line 70

def with_lock
    acquire_lock
    yield
ensure
    release_lock
end