作者归档:admin

VMware虚拟机磁盘压缩备忘

新建的VMware虚拟机,如果不是使用预分配的方式,那么磁盘是动态增长的,这个好处很多,比如可以节约磁盘空间,然后这个磁盘是只增不减的。

VMware提供了压缩的工具。点击虚拟机菜单下的安装VMware Tools(虚拟机必须具备光驱):

挂载光盘,安装工具:

mount /dev/cdrom /mnt
cp VMwareTools-10.1.6-5214329.tar.gz
tar zxvf VMwareTools-10.1.6-5214329.tar.gz
cd vmware-tools-distrib
./vmware-install.pl
open-vm-tools packages are available from the OS vendor and VMware recommends 
using open-vm-tools packages. See http://kb.vmware.com/kb/2073803 for more 
information.
Do you still want to proceed with this installation? [no]  输入yes

一路回车,工具安装完成。

查看磁盘:

/usr/bin/vmware-toolbox-cmd help disk
disk: 执行磁盘压缩操作
用法: /usr/bin/vmware-toolbox-cmd disk <子命令> [参数]

子命令:
   list: 列出可用的位置
   shrink <位置>: 擦除并压缩指定位置的文件系统
   shrinkonly: 压缩所有磁盘
   wipe <位置>: 擦除指定位置的文件系统

/usr/bin/vmware-toolbox-cmd disk list
/
/boot

可以先擦除分区再压缩,也可以同时来:

#擦除:把实际空闲的空间标记为可回收(即可压缩)
/usr/bin/vmware-toolbox-cmd disk wipe /

#压缩
/usr/bin/vmware-toolbox-cmd disk shrinkonly

#擦除并压缩
/usr/bin/vmware-toolbox-cmd disk shrink /

另外,如果需要操作大文件,可以临时挂载一个磁盘,然后在这个磁盘中处理,完成后把该磁盘移除即可。

Linux桌面与VNC服务安装记录

CentOS 7.x中安装VNC:VNC安装的是TigerVnc
安装GNOME Desktop

yum groupinstall "GNOME Desktop" "Graphical Administration Tools"

安装TigerVnc

yum install tigervnc-server

TigerVnc主要安装了两个软件包:

rpm -qa | grep vnc
tigervnc-server-minimal-1.8.0-1.el7.x86_64
tigervnc-server-1.8.0-1.el7.x86_64


rpm -ql tigervnc-server-minimal-1.8.0-1.el7.x86_64
/usr/bin/Xvnc
/usr/bin/vncconfig
/usr/bin/vncpasswd


rpm -ql tigervnc-server-1.8.0-1.el7.x86_64
/etc/sysconfig/vncservers #被/usr/lib/systemd/system/vncserver@.service替换
/usr/bin/vncserver
/usr/bin/x0vncserver
/usr/lib/systemd/system/vncserver@.service
/usr/lib/systemd/system/xvnc.socket
/usr/lib/systemd/system/xvnc@.service

Vncserver实际是Xvnc的包装。默认,VNC针对每个用户开一个桌面,每个桌面对应一个端口号,比如桌面号是:1,说明端口对应5901。

模板配置文件:

[Unit]
Description=Remote desktop service (VNC)
After=syslog.target network.target

[Service]
Type=forking
User=<USER>

# Clean any existing files in /tmp/.X11-unix environment
ExecStartPre=-/usr/bin/vncserver -kill %i
ExecStart=/usr/bin/vncserver %i
PIDFile=/home/<USER>/.vnc/%H%i.pid
ExecStop=-/usr/bin/vncserver -kill %i

[Install]
WantedBy=multi-user.target

这个模板配置中已经说明应该如何操作,首先拷贝这个模板文件,把用真实的用户替代,%i就是桌面号(:1),以下是一个针对root用户的模板:

#######################################
# /usr/lib/systemd/system/vncserver@:1.service
[Unit]
Description=Remote desktop service (VNC)
After=syslog.target network.target

[Service]
Type=forking
User=root

# Clean any existing files in /tmp/.X11-unix environment
ExecStartPre=-/usr/bin/vncserver -kill :1
ExecStart=/usr/bin/vncserver :1
PIDFile=/root/.vnc/%H:1.pid
ExecStop=-/usr/bin/vncserver -kill :1

[Install]
WantedBy=multi-user.target

注意这是针对root用户的,所以PIDFile文件位置是/root/.vnc,如果是普通用户应该是/home/xxx/.vnc。

以下一个自动脚本:

cp /lib/systemd/system/vncserver@.service /lib/systemd/system/vncserver@\:1.service
sed -i 's/%i/:1/g' /lib/systemd/system/vncserver@\:1.service
sed -i 's/<USER>/root/g' /lib/systemd/system/vncserver@\:1.service
sed -i 's/home\/root/root/g' /lib/systemd/system/vncserver@\:1.service

设置密码(注意是在当前用户下),比如是root,就在root下运行vncpasswd,会把密码写入到当前用户的.vnc目录中。

自启动设置:

systemctl enable vncserver@:1.service
systemctl start vncserver@:1.service
systemctl status vncserver@:1.service
systemctl disable initial-setup-text.service

systemctl enable vncserver@:2.service
systemctl start vncserver@:2.service
systemctl status vncserver@:2.service
systemctl disable initial-setup-text.service

以上的脚本实际是/usr/bin/vncserver的包装器,后面的桌面号会锁定到用户,如果进程异常退出,在重启时VNC会自动分配另一个桌面号。锁定的逻辑实际是往tmp中添加一个文件,所以如果进程异常退出需要手动删除锁定文件。

另外,如果修改了/lib/systemd/system/vncserver@\:1.service文件,需要运行systemctl daemon-reload。

命令行用法:

/usr/bin/vncserver -h

usage: vncserver [:<number>] [-name <desktop-name>] [-depth <depth>]
                 [-geometry <width>x<height>]
                 [-pixelformat rgbNNN|bgrNNN]
                 [-fp <font-path>]
                 [-cc <visual>]
                 [-fg]
                 [-autokill]
                 [-noxstartup]
                 [-xstartup <file>]
                 <Xvnc-options>...

       vncserver -kill <X-display>

       vncserver -list

所以如果需要指定分辨率,可以添加-geometry 1440×900, 如果需要指定颜色深度(会影响网络传输流量),可以指定-depth 24

Root用户无法启动Chrome,修改/usr/bin/google-chrome:

if [[ -n "$CHROME_USER_DATA_DIR" ]]; then
  # Note: exec -a below is a bashism.
  exec -a "$0" "$HERE/chrome"  \
    --user-data-dir="$CHROME_USER_DATA_DIR" "$@"
else
  exec -a "$0" "$HERE/chrome"  "$@" --no-sandbox --user-data-dir
fi

Ubuntu中安装VNC(vnc4server):
更新系统:

apt-get update
apt-get upgrade

Gnome桌面环境安装:

apt-get install x-window-system-core
apt-get install gdm
apt-get install ubuntu-desktop
apt-get install gnome-panel gnome-settings-daemon metacity nautilus gnome-terminal

安装VNC

apt-get install vnc4server

dpkg -l | grep vnc
dpkg -L vnc4server
/usr/bin/x0vnc4server
/usr/bin/vnc4passwd
/usr/bin/Xvnc4
/usr/bin/vnc4config
/usr/bin/vnc4server

修改配置:

cp ~/.vnc/xstartup  ~/.vnc/xstartup.bak
vi ~/.vnc/xstartup

#!/bin/sh  
  
export XKL_XMODMAP_DISABLE=1  
unset SESSION_MANAGER  
unset DBUS_SESSION_BUS_ADDRESS  
  
[ -x /etc/vnc/xstartup ] && exec /etc/vnc/xstartup  
[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources  
xsetroot -solid grey  
vncconfig -iconic &  
 
gnome-panel &  
gnome-settings-daemon &  
metacity &  
nautilus &  
gnome-terminal &

启动服务,需要指定桌面号(需要切换到具体的用户):

vncserver -kill :1
vncserver :1

Ubuntu中的vnc4server实际没有提供自启动脚本,启动关闭都需要首先切换到具体用户,并且需要准确对应桌面号,为了开机启动,需要在rc.local中加入类似脚本:

su www -c '/usr/bin/vncserver -name xxx -geometry 1366x768 :10’

注:进程如果异常退出,对应的桌面号可能被锁定。

从Aliyun RDS还原备份数据备忘

从后台下载备份, 可以下载到本地,也可以在远程机器上操作。

安装工具:
https://www.percona.com/doc/percona-xtrabackup/2.2/installation/yum_repo.html

yum install http://www.percona.com/downloads/percona-release/redhat/0.1-3/percona-release-0.1-3.noarch.rpm
yum list | grep percona
yum install percona-xtrabackup-22

rpm -qa | grep percona
percona-release-0.1-3.noarch
percona-xtrabackup-22-2.2.13-1.el7.x86_64

/usr/bin/innobackupex
/usr/bin/xbcrypt
/usr/bin/xbstream
/usr/bin/xtrabackup
/usr/share/doc/percona-xtrabackup-22-2.2.13
/usr/share/doc/percona-xtrabackup-22-2.2.13/COPYING
/usr/share/man/man1/innobackupex.1.gz
/usr/share/man/man1/xbcrypt.1.gz
/usr/share/man/man1/xbstream.1.gz
/usr/share/man/man1/xtrabackup.1.gz

需要使用的是/usr/bin/innobackupex工具。

下载解压工具:http://oss.aliyuncs.com/aliyunecs/rds_backup_extract.sh?spm=5176.7741817.2.6.Aw8bl9&file=rds_backup_extract.sh

chmod +x rds_backup_extract.sh

解压实例备份数据(看数据大小,可能时间较长, 另外确保磁盘空间足够):

rds_backup_extract.sh -f hins3426165_data_xxx.tar.gz -C /root/mysql/data

还原数据:

innobackupex —defaults-file=/root/mysql/data/backup-my.cnf --apply-log /root/mysql/data

启动实例:

mysqld_safe --defaults-file=/root/mysql/data/backup-my.cnf --user=mysql --datadir=/root/mysql/data 

启动实例可能会遇到各种问题,主要是配置文件导致的,注释backup-my.cnf中对应的配置即可:
1 [ERROR] /alidata/server/mysql/bin/mysqld: unknown variable ‘innodb_log_checksum_algorithm=innodb’
把innodb_log_checksum_algorithm=innodb注释掉

2 [ERROR] InnoDB: ./ibdata1 can’t be opened in read-write mode
rm -fr ibdata1 ib*

3 [ERROR] –gtid-mode=ON or UPGRADE_STEP_1 or UPGRADE_STEP_2 requires –log-bin and –log-slave-updates
添加gtid_mode = on,enforce_gtid_consistency = 1,log_slave_updates = 1

4 [ERROR] Fatal error: Can’t open and lock privilege tables: Can’t find file: ‘./mysql/user.frm’
新建用户:
delete from mysql.db where user<>‘root’ and char_length(user)>0;
delete from mysql.tables_priv where user<>‘root’ and char_length(user)>0;
flush privileges;

5 其它问题
一般都是配置相关,对应注释掉相对应的指令即可。

Paypal API

Paypal提供了两套API,REST API和NVP/SOAP API。REST API没有完全覆盖NVP API的功能,NVP API历史久远,未来应该会被REST API替换。

REST API使用OAuth2标准,首先需要有一个开发者账户,然后在开发者账户中创建APP(产生client_id, client_secret),Paypal账户授权给这个APP。

开发者网站:http://developer.paypal.com,然后需要注册一个真实的Paypal账户(可以个人,也可以商用),拿这个账户作为开发者账户去登录,然后就可以创建APP,分两个环境:正式和测试。如何创建一个测试APP,那么会自动给你创建一个测试商家账户,还有一个买家账户。当然也可以自己建多个测试账户。

——————————————————————-
在REST API之前,只有NVP API。为了可以使用NVP API,需要到自己的Paypal中的API设置中取到相关API签名(API用户名和密码,以及签名),只要暴露了这三个信息,就相当于是开放了自己的账户。

每个Paypal可以作为一个第三方,其它的Paypal账户如果把某些权限赋给了它,它就可以扮演第一方的身份去获取信息。关于权限赋予的流程,官方文档有详细描述。而Paypal后台的第三方许可,实际对应这个操作,最终都是赋予第三方权限。

这里需要输入的就是第三方Paypal账户的API的用户名(每个账户中的API签名部分),点击查找后会让你选择哪些权限赋予这个第三方:

结论:Paypal可以通过API签名开放账户,也可以把权限授予其它账户,由其它账户(第三方)代理访问。

<?php

namespace Paypal;

class Api
{
    protected $username = '';

    protected $password = '';

    protected $signature = '';

    protected $version = '95.0';

    protected $endPoint = 'https://api-3t.paypal.com/nvp';

    protected $subject = '';

    public function __construct($username, $password, $signature, $subject = '', $sandbox = false)
    {
        $this->username = $username;
        $this->password = $password;
        $this->signature = $signature;
        if (!empty($subject)) {
            $this->subject = trim($subject);
        }
        if ($sandbox) {
            $this->endPoint = 'https://api-3t.sandbox.paypal.com/nvp';
        }
    }

    // 作为第三方访问Paypal
    public function setSubject($email)
    {
        $this->subject = trim($email);
    }

    // 取回交易列表
    //$params = [
    //    'STARTDATE' => $startTime,
    //    'ENDDATE' => $endTime,
    //    'RECEIVER' => '',
    //    'TRANSACTIONCLASS' => 'All'
    //];
    public function getTransactions(array $params)
    {
        $return = ['success' => 0, 'message' => '', 'data' => []];
        if (empty($params['STARTDATE']) || empty($params['ENDDATE'])) {
            $return['message'] = '参数不合法';
            return $return;
        }

        $result = $this->post('TransactionSearch', $params);
        if (false === $result) {
            $return['message'] = 'CURL请求异常';
            return $return;
        }

        $tarr = explode('&', $result);
        $data = [];
        foreach ($tarr as $item) {
            $tmp = explode('=', rawurldecode($item));

            preg_match('/^L_([a-zA-Z\_]+)([0-9]+)/', $tmp[0], $m);
            if (isset($m[0]) && isset($m[1]) && isset($m[2])) {
                $data[$m[1]][$m[2]] = trim($tmp[1]);
            } else {
                $data[$tmp[0]] = trim($tmp[1]);
            }
        }

        // ACK 等于 Warming时,数据返回不齐全(Paypal每次查询最多返回100条)
        if (empty($data['ACK']) || ($data['ACK'] == 'Failure')) {
            $return['message'] = "API调用ACK返回Failure";
        } else {
            $return['success'] = 1;
            $return['data'] = $data;
        }

        return $return;
    }
 
    // 通用封装
    protected function post($api, array $params)
    {
        $global = [
            'METHOD' => $api,
            'VERSION' => $this->version,
            'USER' => $this->username,
            'PWD' => $this->password,
            'SIGNATURE' => $this->signature
        ];
        if (!empty($this->subject)) {
            $global['SUBJECT'] = $this->subject;
        }

        return $this->doRequest($this->endPoint, array_merge($global, $params));
    }

    // CURL请求
    protected function doRequest($url, $data)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_HEADER, '');
        curl_setopt($ch, CURLOPT_URL, trim($url));
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 90);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
        if (!empty($data)) {
            if (is_array($data)) {
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
            } elseif (is_string($data)) {
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
            }
        }
        if (\PHP_OS === 'WINNT') {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        }
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0");
        $result = curl_exec($ch);
        $error = curl_errno($ch);
        curl_close($ch);
        if ((int)$error == 0) {
            return $result;
        }
        return false;
    }
}

事件循环:JavaScript、Node.js、PHP-Swoole、PHP-workerman

在浏览器端,JavaScript的主要是与用户互动,以及操作DOM。为了避免复杂性,JavaScript被设计为只能运行在单进程单线程中。H5提出了Web Worker标准,允许JavaScript创建多个线程,但子线程受主线程控制,且不得操作DOM。

在JavaScript中,任务分为同步任务和异步任务(异步任务不堵塞当前线程),所有同步任务都在主线程上执行,形成一个执行栈,另外存在一个“任务队列”,如果异步任务有了结果,就在“任务队列”中添加一个事件(对应回调),一旦“执行栈”中的所有同步任务执行完毕,就会读取“任务队列”,取出事件对应的回调,然后进入执行栈执行回调(注:任务队列中对应的是事件,事件可以对应多个回调,回调的执行也依赖事件对象记录的原始参数)。

事件的产生包括用户比如用户点击,异步调用后触发的事件,取出事件是一个循环过程(执行栈空),所以这个过程叫事件循环(event loop)。

Node.js中的事件循环原理上和JavaScript中的(应该是浏览器)并没有很大不同。Node.js中为了对付大量的链接,使用了epoll,对于异步任务,使用了一个线程池来模拟,回调的执行全部落在主线程中,所以它是单进程单线程的(这个也是为何面对CPU密集运算时的场景不合适,尽管还有其它的CPU是空闲的,因为执行回调的,仅一个线程)。

PHP中的一个扩展Swoole,理念上和Node.js是差不多的,但是架构上有很大不同。Swoole启动后,首先启动一个主进程,在这个进程内其它若干个React线程,这些线程专门负责监听,接收数据和响应,然后启动一个manager进程和一组Worker进程和若干task进程,manager进程主要用来监控worker进程和task进程(比如退出重启等),Worker可以把耗时的任务投递给task进程,task执行时同步堵塞的,执行完毕后通过进程间通信的方式通知Worker进程。每个Worker进程维护一个事件循环,并在Worker进程内执行回调(可以应用到多核CPU)。

PHP-workerman相对Swoole来说,架构上就比较简单。它相当于只有Swoole的Worker进程这部分。每个Worker都相互独立的监听端口,执行回调,响应数据等。

MySQL 事务与锁查看

information_schema.innodb_trx

trx_id					事务ID
trx_state				事务状态
trx_started				事务执行开始时间
trx_requested_lock_id			事务等待锁ID号(等待其它事务释放锁)
trx_wait_started			事务等待锁开始时间
trx_weight						
trx_mysql_thread_id			事务线程ID
trx_query				具体的SQL
trx_operation_state			事务当前操作状态
trx_tables_in_use			事务中有多少个表被使用
trx_tables_locked				
trx_lock_structs					
trx_lock_memory_bytes			事务锁住的内存大小(B)
trx_rows_locked				事务锁住的行数
trx_rows_modified			事务更改的行数
trx_concurrency_tickets			事务并发数
trx_isolation_level			事务隔离级别
trx_unique_checks			是否唯一性检查
trx_foreign_key_checks			是否外键检查
trx_last_foreign_key_error		最后的外键错误
trx_adaptive_hash_latched		
trx_adaptive_hash_timeout		

注:trx_started记录了事务开始的时间,如果过去了很长时间,可能是异常事务。trx_wait_started记录了等待时间,如果等待了很长时间,可能是异常事务。对应等待锁的事务,trx_query记录了具体的SQL语句。trx_mysql_thread_id可以定位到具体的线程(回话ID)

information_schema.innodb_locks

lock_id							锁ID
lock_trx_id						拥有锁的事务ID
lock_mode						锁模式
lock_type						锁类型
lock_table						被锁的表
lock_index						被锁的索引(类型)
lock_space						被锁的表空间号
lock_page						被锁的页号
lock_rec						被锁的记录号
lock_data						被锁的数据(对应索引编号,一般是ID号)

事务可以持有多个锁。锁类型有S和X。根据索引类型不同,lock_rec和lock_data可以定位行号。当开启一个事务,对相关行上锁,这个时候的锁不会出现在innodb_locks表中,只有相关的锁被其它事务等待时,产生了锁等待,才会把锁与等待的锁插入此表(换个说法就是此表是用来存放有依赖关系的锁的)

information_schema.innodb_lock_waits

requesting_trx_id					请求锁的事务ID
requested_lock_id					请求锁的锁ID
blocking_trx_id						当前拥有锁的事务ID
blocking_lock_id					当前拥有锁的锁ID
requesting_thd_id					请求锁的线程ID
blocking_thd_id						当前拥有锁的线程ID

记录依赖关系。请求锁等待持有锁。

1 当要查看有哪些线程时,直接运行show full processlist即可,这个命令动态列出当前的线程状态
2 当要查看有哪些事务时(比如检查有哪些事务长时间未结束),可以直接查看innodb_trx表,这个表中的trx_started记录了开始事务的时间。
3 当要查看是否有锁等待时,可以查看innodb_locks,只要有记录,就说明产生了锁等待,具体是哪个依赖哪个,需要查看innodb_lock_waits的关系。

一般来说,产生了锁等待,如果超时,事务会自动释放,但是如果事务开启了,单长时间没有结束,就应该去innodb_trx查看确认(从线程基本无法查看到已经开启了事务)。

获得导致行锁等待和行锁等待超时的会话:

select l.* from ( select 'Blocker' role, p.id, p.user, left(p.host, locate(':', p.host) - 1) host, tx.trx_id, tx.trx_state, tx.trx_started, timestampdiff(second, tx.trx_started, now()) duration, lo.lock_mode, lo.lock_type, lo.lock_table, lo.lock_index, tx.trx_query, lw.requesting_thd_id Blockee_id, lw.requesting_trx_id Blockee_trx from information_schema.innodb_trx tx, information_schema.innodb_lock_waits lw, information_schema.innodb_locks lo, information_schema.processlist p where lw.blocking_trx_id = tx.trx_id and p.id = tx.trx_mysql_thread_id and lo.lock_id = lw.blocking_lock_id union select 'Blockee' role, p.id, p.user, left(p.host, locate(':', p.host) - 1) host, tx.trx_id, tx.trx_state, tx.trx_started, timestampdiff(second, tx.trx_started, now()) duration, lo.lock_mode, lo.lock_type, lo.lock_table, lo.lock_index, tx.trx_query, null, null from information_schema.innodb_trx tx, information_schema.innodb_lock_waits lw, information_schema.innodb_locks lo, information_schema.processlist p where lw.requesting_trx_id = tx.trx_id and p.id = tx.trx_mysql_thread_id and lo.lock_id = lw.requested_lock_id) l order by role desc, trx_state desc;

对于复杂的多个会话相互行锁等待情况,建议先终止 Role 为 Blocker 且 trx_state 为 RUNNING 的会话;终止后再次检查,如果仍旧有行锁等待,再终止新结果中的 Role 为 Blocker 且 trx_state 为 RUNNING 的会话。

对于标识为 Blocker 的会话(持有锁阻塞其他会话的 DML 操作,导致行锁等待和行锁等待超时),确认业务可以接受其对应的事务回滚的情况下,可以将其终止。比如,可以通过 Kill 命令来今后会话终止。

浏览器编程:WebDriver – ChromeDriver

浏览器编程有两个主要应用:
1 自动化测试
2 通过浏览器自动抓取内容(针对防抓的网站,模拟人工点击)

Selenium Server只是作为一个代理,它的作用是当要驱动远程浏览器(驱动一般只能监听本地端口),或需要驱动不同版本的浏览器时会有很大的作用。否则,应用程序直接面对具体的驱动即可(Selenium Server仅转发Json)。
注意:PhantomJS视乎是没有提供驱动,为了驱动这个无头浏览器,Selenium Server应该是把Json数据转换成了JS脚本让其执行(未证实)

目前主流浏览器(Chrome Firefox)都提供了WebDriver的实现,比如Chrome对应的是ChromeDriver,Firefox对应的FirefoxDriver。注:WebDriver只是一个规范标准(https://w3c.github.io/webdriver/webdriver-spec.html),而实现的方式可以不同,而https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol这里描述的就是一种实现方式,任何实现了这个规范的驱动,都具有相同的API。

WebDriver is an open source tool for automated testing of webapps across many browsers. It provides capabilities for navigating to web pages, user input, JavaScript execution, and more. ChromeDriver is a standalone server which implements WebDriver’s wire protocol for Chromium. ChromeDriver is available for Chrome on Android and Chrome on Desktop (Mac, Linux, Windows and ChromeOS).
ChromeDriver是一个实现了WebDriver无线协议的独立服务器,所以需要下载这个服务器(驱动,启动后在本地监听9515端口,所有的操作发送到9515端口,这个驱动负责解析数据并操作浏览器,所以它是一个中间件)。

下载地址:https://sites.google.com/a/chromium.org/chromedriver/downloads(有三个平台,需要注意的是不同的版本对应的Chrome版本是不同的)。

关于使用,ChromeDriver提供了一个文档:
https://sites.google.com/a/chromium.org/chromedriver/getting-started(关键点:Chrome需要安装在默认位置(否则需要指定),下载正确的ChromeDriver版本)

ChromeDriver作为一个独立的服务,可以手动启动并监控,也可以在使用SDK中提供的方法启动。

#####
@RunWith(BlockJUnit4ClassRunner.class)
public class ChromeTest extends TestCase {

  private static ChromeDriverService service;
  private WebDriver driver;

  @BeforeClass
  public static void createAndStartService() {
    service = new ChromeDriverService.Builder()
        .usingDriverExecutable(new File("path/to/my/chromedriver"))
        .usingAnyFreePort()
        .build();
    service.start();
  }

  @AfterClass
  public static void createAndStopService() {
    service.stop();
  }

  @Before
  public void createDriver() {
    driver = new RemoteWebDriver(service.getUrl(),
        DesiredCapabilities.chrome());
  }

  @After
  public void quitDriver() {
    driver.quit();
  }

  @Test
  public void testGoogleSearch() {
    driver.get("http://www.google.com");
    // rest of the test...
  }
}

####独立启动
$ ./chromedriver
Started ChromeDriver
port=9515
version=14.0.836.0

WebDriver driver = new RemoteWebDriver("http://127.0.0.1:9515", DesiredCapabilities.chrome());
driver.get("http://www.google.com");

####PHP
$service = new \Facebook\WebDriver\Chrome\ChromeDriverService(‘path/to/my/chromedriver’, 9515);
$service->start();
$service->stop();

$driver = RemoteWebDriver::create( $service->getURL(),[
                ChromeOptions::CAPABILITY => $options,
                WebDriverCapabilityType::PROXY => [
                    'proxyType'=> 'manual',
                    'httpProxy' => 'SOCKS5://127.0.0.1:1086',
                    'sslProxy' => 'SOCKS5://127.0.0.1:1086',
                    'socksProxy' => 'SOCKS5://127.0.0.1:1086'
                ]]);

ChromeDriver实际释放的是一套RESTfull API,可以参考:https://chromium.googlesource.com/chromium/src/+/master/docs/chromedriver_status.md,所以只要按照规范发送数据即可,也可以使用SDK,比如PHP,Facebook提供了一套实现:

composer require facebook/webdriver

应用实例:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Dusk\Chrome\ChromeProcess;
use Laravel\Dusk\Browser;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\WebDriverCapabilityType;
use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;

class Test extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'test';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //$process = (new ChromeProcess())->toProcess();
        //$process->start();

        try {
            // 取回已经打开的SessionID
            $reuseSessionId = '51492cf202343defea198867e32a81e3';
            try {
                $driver = $this->driverBy($reuseSessionId);
            } catch (\Exception $e) {
                $driver = $this->driver();
            }

            $driver->execute(DriverCommand::CLICK);
        } catch (\Exception $e) {
            echo 'Browser can not start up: ' . $e->getMessage();
            return;
        }

        // 取回所有SESSION
        print_r($driver->getAllSessions('http://localhost:9515'));

        $sessionID = $driver->getSessionID();
        echo "\n";
        echo $sessionID;
        echo "\n";

        $browser = new Browser($driver);
        $browser->visit('https://www.baidu.com/')->type("#kw", 'ip')->press("#su");
        $browser->visit('https://www.amazon.com');

        $driver->close();
        $driver->get('http://blog.ifeeline.com');
    }

    protected function driverBy($sessionId)
    {
        $driver = RemoteWebDriver::createBySessionID($sessionId, 'http://localhost:9515');
        $driver->execute(DriverCommand::CLICK);

        return $driver;
    }

    protected function driver()
    {
        $options = (new ChromeOptions)->addArguments([
            //'--disable-gpu',
            //'--headless'
        ]);

        return RemoteWebDriver::create(
            'http://localhost:9515',
            [
                ChromeOptions::CAPABILITY => $options,
                WebDriverCapabilityType::PROXY => [
                    'proxyType'=> 'manual',
                    'httpProxy' => 'SOCKS5://127.0.0.1:1086',
                    'sslProxy' => 'SOCKS5://127.0.0.1:1086',
                    'socksProxy' => 'SOCKS5://127.0.0.1:1086'
                ]
            ]
        );

        /*
        return RemoteWebDriver::create(
            'http://localhost:9515',
            DesiredCapabilities::chrome()->setCapability(
                ChromeOptions::CAPABILITY, $options
            )->setCapability(
                WebDriverCapabilityType::PROXY,
                [
                    'proxyType'=> 'manual',
                    'httpProxy' => 'SOCKS5://127.0.0.1:1086',
                    'sslProxy' => 'SOCKS5://127.0.0.1:1086',
                    'socksProxy' => 'SOCKS5://127.0.0.1:1086'
                ]
            )
        );
        */
    }
}

在控制浏览器上,可以应用了–disable-gpu和–headless,这样就是一个无头浏览器了(不显示具体的过程)。另外,在创建的浏览器时,可以指定代理。另外,如果一个SESSION没有正确退出,那么它还是活动的,但是它却无法重用。在知道SESSIONID的情况下,SESSION可以重用。 一般来说,进行一个任务就开启一个浏览器,完毕后正常退出记录。

Laravel中的Dusk程序包,封装的更加狠一些,连ChromeDriver二进制程序包都拉取回来,自动启动监听,对个Facebook SDK进行二次封装,使API更加友好。

如果模拟人工进行大量操作,就会频繁启动关闭浏览器,实际上,浏览器启动后对应一个SESSION,接下来只要重用这个SESSION即可,基本思路:如果当前有可重用的SESSION,就重用,没有就新建;在任务执行完后,判断SESSION是否超过了最大值,超过则关闭,否则,标记该SESSION可重用(配合定时重启脚本,防止意外)。

Mac 定义自己的系统

苹果Mac系列产品:
MacBook Air 轻薄本,主流11和13英寸
MacBook Pro 笔记本,主流13,15英寸
Mac mini 一个盒子(可看做是一个主机,普通主机或服务器)
iMac 一体机
Mac Pro 台式机

OS X扫盲
OS X是一个基于Unix Darwin内核构建的系统。

每个用户对应/Users目录下的一个以用户名称命名目录(一般是如此)。所有与用户相关的内容都在这个目录内。

Finder是一个资源管理器。
Dock是一个工具栏,是一个资源访问的快捷方式。

OS X中,磁盘映像文件后缀名是dmg,双击dmg文件可以直接打开,然后在Finder左边的设备总可以找到挂接好的磁盘映像。

OS X中,安装一个软件实际就是打开dmg文件并启用的应用拖入/Application,卸载就是从其中删除。

OS X 下的大部分软件具备状态保持的功能(记住上次打开的状态)

快捷键
Mac的键盘跟Windows键盘有一些不一样。有些键没有,比如Home和End键和PageUP和PageDown和Backspace,另外ALT键也叫Option键,Windows中的Wind键对应Command键。

具体的快捷键可以参考:https://support.apple.com/zh-cn/HT201236

最常用的复制粘贴等组合键,在Mac中用Command对应Control键:Control + C对应Command + C。

为了高效工作,以下键必须记一记:

Command + Tab		向前循环切换应用程序
Shift + Command + Tab	向后循环切换应用程序
Command + Delete	把选中的资源移到废纸篓
Shift + Command + Delete清空废纸篓(清空回收站,提示)
Command + ~		同一个应用多窗口切换(开了多窗口是有用)
Command + C/V		复制粘贴
Command + Option + V	移动(先复制,后粘贴,类似剪贴)
Command + N		新建应用程序窗口
Command + Q		退出应用程序(一个应用可以开多窗口,窗口的叉表示关闭窗口,而应用未关闭)
Command + +/-		放大缩小字体
Control + Space		输入法切换
Command + Space		调出Spotlight


FN + 左键 		HOME
FN + 右键     		END
FN + 上键 		PageUP
FN + 下键 		PageDown
FN + DELETE 		后删除(CTR + D)

比较遗憾的是,最小化所有窗口没有找到对应的快捷键(Option + Command + H再结合Command + M)。

鼠标与触摸板
系统偏好设置 – 鼠标

其中滚动方式:自然,默认是勾上的,这个和Window下的默认设置刚好相反,所以如果不习惯需要去掉勾选。

系统偏好设置 – 触控板

1 光标与点按
一般都勾上,单个手指轻按模拟鼠标左键点击,两个手指轻安模拟鼠标右击,三个手指轻击进行文本查询(比如翻译)
2 滚动与缩放
两个手指触摸移动模拟鼠标滚动,不勾上时,方向和Window一样。
3 更多手势
三个手指向上滑动(Mission Control),四个手指抓(LaunchPad),四个手指快速展开(显示桌面)

另外,如果需要使用触控板来实现拖动窗口,就复杂一些:
系统偏好设置 – 辅助功能 – 鼠标与触控板 – 触控板选项

启动三指拖动。

Dock
Dock就是一个停靠条。默认放在最下方。可以通过简单拖放的方式,把应用放到Dock中,以一目了然地知道启动了那些应用以及快速管理常用的应用。

注意:连按窗口标题栏以 这里默认是最小化,可以宣威缩放,这样就可以实现最大化。

AppStore
这个就是安装App的入口,里面有非常多的软件,一般建议注册个apple id,然后通过AppStore来进行安装应用,AppStore是无法包含所有应用。可以直接下载第三方应用镜像包来进行安装。一般是dmg镜像,双击自动进行挂载斌打开镜像包,然后把应用直接拖入Application文件夹就完成安装(当然也可以来一个安装步骤导向)。


工具:

1 搜索
Spotlight 是OS X 自带的搜索工具(Command + Space)。

2.Launcher(应用程序 – Launcher,有对应手势:抓动作)
Launcher(启动器)的主要作用之一就是快速定位并启动应用程序。

开发工具:
SecureCRT(收费)
Chrome + SwitchyOmega
FireFox
SourceTree
PhpStorm(收费)
Navicat(收费)
TeamView
VMware Fusion(收费)
VirtualBox
FileZilla

Microsoft Office (收费)
钉钉
QQ

Avast Security(家庭免费版)
ShadowsocksX-NG
PDF Reader

Mac 文件共享与远程虚拟机映射

在Mac中设置共享文件夹:

系统偏好设置 – 共享 – 文件共享:选择要共享的文件夹(不能自定义名字),选择用户和用户权限,然后点击“选项”:

如果使用SMB来共享,需要按照”Windows文件共享”说明来设置(大体就是设置一个跟系统不一样的密码,这个密码专门让Window来存储)。

在远程机器上,可以mount这个共享:

#CentOS 
yum install cifs-utils
#Ubuntu
sudo apt-get install cifs-utils

然后安装如下脚本格式进行Mount:

#mount共享文件夹
mount -t cifs //192.168.1.121/www /var/www -o username=administrator,password='xxx',uid=1000,gid=1000

如果远程机器是Windows,那么直接使用映射即可。

其它参考:http://blog.ifeeline.com/2512.html

Mac 开发环境搭建

进入Mac的默认Shell终端,安装Homebrew工具:

#https://brew.sh/index_zh-cn.html
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Homebrew是一个Ruby工具,类似Ubuntu下的apt-get和CentOS下的YUM。后面安装的软件就依靠它来完成。

关于Homebrew,还需要知道的:
通过brew安装的软件,默认总是安装/usr/local/Cellar这个目录中,其它方式的组织是通过软链接的方式链接到对应的文件。
通过brew安装的软件,一般可以把/usr/local看做跟目录,比如工具对应的配置和可执行文件,分别位于或对应于/usr/local/etc/xxx,/usr/local/bin/xxx,/usr/local/sbin/xxx;其中的可执行文件均为符合链接。
搜索软件使用brew search xxx, 安装使用brew install xxx,卸载使用brew uninstall xxx。

另一个需要注意的是:Mac当前登录的用户,是普通用户,如果某个软件需要以root权限启动,那么就需要首先打入sudo来临时切换到以root身份运行某个命令。

一 安装PHP

#搜索,会出现几个分之,比如PHP56 PHP71
brew search php
#过滤,只要71分支,提供了非常多扩展包
brew search php71
#安装(选择需要的扩展包)
brew install homebrew/php/php71 homebrew/php/php71-apcu homebrew/php/php71-redis homebrew/php/php71-mongodb homebrew/php/php71-opcache omebrew/php/php71-swoole

大部分PHP的模块,都包含在了homebrew/php/php71中,是编译到内核的(非动态模块),上面的apcu,redis,mogondb,swoole是动态模块,模块安装位置:/usr/local/opt/,比如:/usr/local/opt/php71-apcu/apcu.so。配置文件自然是/usr/local/etc/php/7.1/php.ini,扩展的配置放在/usr/local/etc/php/7.1/conf.d/*.ini。

php -v
php -m

编译到内核的模块确实是大而全,然后还需要调整一下php.ini的配置(才能符合开发环境要求):

#设置时区
date.timezone = Asia/Shanghai
 
#CGI相关参数,实际上建议修改的是force_redirect,其它均保留默认值
cgi.force_redirect = 0   #默认为1,改为0
cgi.fix_pathinfo = 1     #默认是1,保留
fastcgi.impersonate = 1  #默认是1,保留
cgi.rfc2616_headers = 0  #默认是0,保留

#其它参数调整,根据实际情况调整
upload_max_filesize = 64M
max_execution_time = 1200
max_input_time = 600
max_input_nesting_level = 128
max_input_vars = 2048
memory_limit = 1024M
 
post_max_size = 64M

如果要启动PHPFPM,FPM主配置文件/usr/local/etc/php/7.1/php-fpm.conf,池配置在/usr/local/etc/php/7.1/php-fpm.d中,需要注意的是,池配置中,默认的运行用户是和用户组均为_www,所以需要检查文件的权限,保证对_www具有读和执行(默认是符合的),如果要写入,那么还需要保证对应的文件夹有被写入的权限。

启动PHPFPM,由于php-fpm这个命令放入到了/usr/local/sbin中,默认shell并不搜索这个路径,所以要想添加环境变量:

#设置环境变量
export PATH="/usr/local/sbin:$PATH"  
echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.bash_profile

#确认命令能找到
which php-fpm
which php71-fpm

#手动启动,PHPFPM可以不使用root身份启动(user和group指令无用),会使用当前用户运行
sudo php71-fpm start
sudo php71-fpm stop

对于开发环境,PHPFPM可以不用启动,直接使用PHP内置的HTTP服务器也可以。

二 安装Nginx

brew install --with-http2 nginx  

如果要绑定到80端口,那么Nginx就必须以root身份运行。默认的server配置位于(可改):/usr/local/etc/nginx/servers。可以往里面方式配置:

server {
    listen 80;
    #listen 443 ssl http2;
    server_name test.app;
    root "/Users/xx/www/test/public";
 
    index index.html index.htm index.php;
 
    charset utf-8;
 
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }
 
    access_log off;
 
    sendfile off;
 
    location ~ \.php$ {
        client_max_body_size 64M;
        fastcgi_intercept_errors off;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 64 32k;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
 
    location ~ /\.ht {
        deny all;
    }
 
    #ssl_certificate     /etc/nginx/ssl/test.app.crt;
    #ssl_certificate_key /etc/nginx/ssl/test.app.key;
}

Nginx的主配置文件设置的启动user一般应该和PHPFPM相同,或者需要保证Nginx对文件具备读和执行的权限。如果是文件上传,还需要确保Nginx对临时中间文件夹具备写入权限。

启动关闭等:

sudo nginx -t
sudo nginx -s start
sudo nginx -s stop

三 安装MySQL

#安装最新版本(5.7.xx)
brew install mysql

#确定搜索路径:
which mysqld
mysqld —verbose —help | grep -A 1 ‘Default options’

/etc/my.cnf  /etc/mysql/my.cnf  /usr/local/etc/my.cnf  ~/.my.cnf

#
mysql.server start
mysql_secure_installation

# 停止
mysql.server stop

MySQL不需要以root身份启动。

关于启动问题:
在Mac下,如果要开机启动,可以参考如下配置(一般不需要):

#Nginx
cp /usr/local/opt/nginx/homebrew.mxcl.nginx.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  

# PHP-FPM
cp /usr/local/opt/php70/homebrew.mxcl.php71.plist ~/Library/LaunchAgents/  
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  

# MySQL
cp /usr/local/opt/mysql/homebrew.mxcl.mysql.plist ~/Library/LaunchAgents/  
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

## 卸载
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

更加实际的方法:进入操作系统后,启动Nginx和PHPFPM(因为要sudo,需要输入密码),MySQL则在需要时启动,比如本地一般链接远程数据库。所以可以这个别名:(往.bash_profile中写入)

alias servers.start='sudo nginx && php-fpm --fpm-config /usr/local/etc/php/7.1/php-fpm.conf -D'
alias servers.stop='sudo bash -c "killall -9 php-fpm && nginx -s stop"'                       
alias nginx.logs='tail -f /usr/local/opt/nginx/access.log'
alias nginx.errors='tail -f /usr/local/opt/nginx/error.log'

遇到问题:
1 Nginx启动提示

nginx: [emerg] getgrnam("

提示大体就是找不到用户组的意思。在Nginx配置中,user如果只指定了用户名,默认会去寻找同名的用户组,在Mac中,用户不一定对应一个同名的用户组,所以出现这种情况就是需要明确指定存在的用户组,可以通过如下方式来确定用户和用户组:

#当前登录的用户名
whoami
www

#确认用户组(可见www的uid是502,对应的组id是20,名称是staff)
id
uid=502(www) gid=20(staff) groups=20(staff),12(everyone)

把www和staff对应填入,错误提示消失。