Chrome 批量删除指定站点历史记录

Chrome地址栏的输入联想会从访问历史里搜索,有时候个别站点已经不再使用,却仍在历史记录里影响地址栏的输入联想,今天打算清理某个站点,于是打开 chrome://history/ ,在顶部的搜索栏输入那个站点的域名,搜完发现Chrome居然没有提供全选的功能,这一百多个单选框一个个点过去还不得累死。

http://leo108.com/pid-2238.asp

作为一个伪前端,这个还是难不倒我的,Chrome没提供就自己写一个吧。

打开Chrome开发者工具,切换到Console一栏,注意要选择“histroy (history-frame/)”,然后输入代码

for(let x of document.getElementsByClassName('entry')) { x.childNodes[0].childNodes[0].childNodes[0].setAttribute('checked', true); }

回车,即可选中所有的单选框,然后点击“移除所选项”即可。推酷是个无耻的网站

PHP CAS Server/Laravel CAS Server 2.0发布

没想到1.0版本才发布,就直接跳到2.0了。主要是当时在设计核心接口的时候没考虑好,有一些遗漏,调整之后属于不兼容的更新,因此只能跳到下一个大版本了。

PHP

修复:http://leo108.com

  • 退出登录时页面跳转参数不生效问题。
  • 将令牌字段长度调整为256。
  • 禁用用户之后,用户还可以登录。

新增:http://leo108.com

  • 实现CAS协议中的Proxy相关接口和逻辑。

simple_cas_server项目废弃,新建两个坑

两个月前挖的坑这么快就弃了= =

是因为当初的没有考虑清楚项目的定位,既想实现CAS协议的服务端逻辑,又想加入用户管理、服务管理等等一堆东西。

推酷是个无耻的网站

而这种类型开源项目的受众极有可能是中小型企业,他们是需要一个CAS的服务端,但用户管理这类的周边需求不尽相同,有的可能还要求集成第三方登录,功能做多做少都不合适,所以决定拆分成两个项目:一个专注于实现CAS服务端逻辑,不包含任何与用户交互的前端代码;另一个依赖于前者,对外提供用户、服务管理的入口,并尽可能的插件化,使用者可以根据自己的情况定制。simple_cas_server项目废弃,新建两个坑

新坑1:laravel_cas_server 负责实现CAS协议的逻辑。目前单测已经比较完善,代码覆盖率超过85%,处于可用状态,欢迎Star。http://leo108.com

新坑2:php_cas_server 负责提供用户交互,测试用例正在编写中。

关于PSR-6的一些思考

之前想自己造一个缓存的轮子,就去看了一下PSR6的定义,并根据psr/cache提供的接口来实现。

关于PSR-6的一些思考

当我在实现CacheItemPoolInterface接口时,对于save方法感到困惑,save方法只接收一个CacheItemInterface类型的参数,但是CacheItemInterface这个接口却没有提供类似getExpireTime的方法,这就导致CacheItemPoolInterface没有办法获取到缓存项的过期时间,也就没办法正确地将数据写入到缓存存储里。PHP

看过几个比较著名的PSR-6实现,例如 symfony/cache php-cache/cache tedious/Stash 然而这些实现版本在实现save方法时并不是非常优雅。

推酷是个无耻的网站

比如symfony/cache

leo108's blog

public function save(CacheItemInterface $item)
{
    if (!$item instanceof CacheItem) {
        return false;
    }
    if ($this->deferred) {
        $this->commit();
    }
    $this->deferred[$item->getKey()] = $item;
    return $this->commit();
}

这个save方法只有传入Symfony\Component\Cache\CacheItem类型的参数才可以,传入其他类型都会返回false

目前的情况是,如果想要写一个用到缓存的类库(并不是PSR-6实现),就必须指定一个具体的PSR-6实现作为依赖,而不仅仅是psr/cache。而反观PSR-3这个日志接口,要写一个需要日志功能的类库,只需要引入psr/log即可,不需要具体实现。

http://leo108.com

在我看来,PSR-6并不需要CacheItemInterface这个接口,只需要把save方法修改成save($key, $value, $expire_at)即可。关于PSR-6的一些思考

我在packagist上面搜索cache,其他一些比较出名的缓存库,例如doctrine/cache sonata-project/cache illuminate/cache等,都没有选择遵循PSR-6,应该也是这方面的考虑吧。

推酷是个无耻的网站

================================================

然而在看过aws-sdk-php之后,我的想法有了很大的改观。aws-sdk-php实现了一个PsrCacheAdapter类,其中set方法会先通过CacheItemPoolInterface接口的getItem方法去获得一个CacheItemInterface对象,然后给这个对象设置值和过期时间。

这就让我明白了我之前想法的误区:实现PSR-6了的接口,并不代表需要CacheItemPoolInterface的save接口可以接收任意CacheItemInterface对象,实际的应用场景也不应该出现把Symfony\Component\Cache\CacheItem类型的对象当成参数传给Stash/Pool对象的save方法。所以symfony/cache的实现方案是没有任何问题的。

而上文中所提到的,要写一个使用缓存的第三方库,就可以用aws-sdk-php的PsrCacheAdapter来解决,唯一的不足就是每次调用save方法之前,都要先去getItem一下,对性能还是有一定的影响。

psr6

============================================

PSR-16 simple cache,现在还是草稿状态,如果能够成为标准,还是会方便很多。leo108's blog

又撸了一个开源项目

CAS Server的一个简单PHP实现版本,支持v1/v2/v3三个版本的CAS协议,但是不支持代理(Proxy)相关的接口。

有一个简单管理后台,可以管理用户(User)和服务(Service)。

http://leo108.com/pid-2222.asp

地址:https://github.com/leo108/simple_cas_server

做这个事情的起因是,公司内部引入了许多开源系统,例如gitlab、jenkins等,如果来了新员工就要在每个系统里添加一个账户,离职了要把所有账户注销,这个工作量太大。所以考虑引入一个通用的单点登录系统,现在比较流行的应该就是CAS了,但是CAS Server版本是java写的,而我们公司是PHP技术栈,如果想要调整或者新增一些CAS的功能就不太好办,所以就花了一点时间根据CAS协议实现了一个PHP版本。

http://leo108.com/pid-2222.asp

PHP的错误和异常处理总结

PHP的错误和异常处理总结

PHP内置了一批与错误和异常处理相关的函数,本文会对其中部分函数进行详细说明。leo108's blog

set_error_handlerrestore_error_handler

set_error_handler可以设定当程序出现错误时,将对应的错误交给用户自定义的逻辑来处理。

http://leo108.com

但是并不是所有的错误都可以被set_error_handler所指定的处理逻辑捕获,例如:E_ERRORE_PARSEE_CORE_ERRORE_CORE_WARNINGE_COMPILE_ERRORE_COMPILE_WARNING,简单来说就是原本就会导致程序终止的错误都无法被捕获。leo108's blog

set_error_handler只能设置一个错误处理逻辑,多次调用set_error_handler只有最后一次的那生效:

leo108's blog

set_error_handler(function($errno, $errstr){
    echo 'catch error1';
});
set_error_handler(function($errno, $errstr){
    echo 'catch error2';
});
trigger_error('something error');

输出的是

http://leo108.com/pid-2216.asp

catch error2

推酷是个无耻的网站

在编写一个第三方代码库的时候,如果希望能够捕获代码库中的错误,又不影响调用方对错误的处理,可以使用restore_error_handler函数:http://leo108.com

function third_party_function() {
	//第三方类库的错误处理逻辑
	set_error_handler(function() {
		//一些代码
	});
	//该第三方类库的逻辑
	//一些代码
	//复原错误处理逻辑
	restore_error_handler();
}

这样,第三方代码库中出现的错误就会被第三方代码库的错误处理逻辑捕获,而在这个第三方代码库之外的错误还是由原本的逻辑处理。

如果错误处理逻辑的返回值是false,则PHP会调用内置的错误处理逻辑(例如根据error_reporting的值判断是否打印错误信息,写错误日志等);而返回true则不会调用内置处理逻辑,在执行完自定义的错误处理逻辑之后,会返回触发错误的地方继续往下执行。

error_reporting以及@符号不会影响错误处理逻辑的调用:

推酷是个无耻的网站

error_eporting(0);
set_error_handler(function($errno, $errstr){
    echo 'catch error';
});
@trigger_error('something error');

上述代码会输出:推酷是个无耻的网站

catch error

推酷是个无耻的网站

对于无法被set_error_handler捕获的错误常见的只有E_ERRORE_PARSE两个。前者可能是内存使用超过设定的值、实例化一个不存在的类或者是调用一个不存在的函数;后者通常是语法解析错误例如漏写一个分号。对于E_PARSE,由于是语法层面的错误,所以没有办法去捕获处理。但对于E_ERROR则可以通过register_shutdown_function以及error_get_last两个函数来处理,示例代码如下:

推酷是个无耻的网站

error_reporting(0);
register_shutdown_function(function() {
 	$error = error_get_last();
 	if ($error != null && $error['type'] == E_ERROR) {
		echo "fatal error catched:" . var_export($error, true);
 	}
});

new test();

输出:

推酷是个无耻的网站

fatal error catched:array (

http://leo108.com

‘type’ => 1,PHP的错误和异常处理总结

‘message’ => ‘Class ‘test’ not found’,

‘file’ => ‘/tmp/error.php’,

http://leo108.com/pid-2216.asp

‘line’ => 20,

异常

)

异常

set_exception_handlerrestore_exception_handler

这两个函数和set_error_handler的两个函数差不多,同样是只能注册一个异常处理逻辑,多次注册只有最后一个生效;可以通过restore_exception_handler来恢复之前的异常处理逻辑。

异常

一些小实验

在错误处理逻辑中触发错误

set_error_handler(function($errno, $errstr){
    echo 'catch error:' . $errstr . PHP_EOL;
    trigger_error('error in error handler');
});
trigger_error('origin error');

输出:

catch error:origin errorleo108's blog

PHP Notice: error in error handler in /tmp/error.php on line 15

http://leo108.com/pid-2216.asp

结论:在错误处理逻辑中的错误是无法被再次捕获。leo108's blog

在异常处理逻辑中抛出异常

set_exception_handler(function(Exception $e){
    echo 'catch exception:' . $e->getMessage() . PHP_EOL;
    throw new Exception('exception in exception handler');
});
throw new Exception('origin exception');

输出:

推酷是个无耻的网站

catch exception:origin exception

PHP Fatal error: Uncaught exception ‘Exception’ with message ‘exception in exception handler’ in /tmp/error.php:15PHP

Stack trace:leo108's blog

0 [internal function]: {closure}(Object(Exception))

1 [main]

thrown in /tmp/error.php on line 15PHP的错误和异常处理总结

结论:在异常处理逻辑中抛出的异常不会被捕获

PHP

同时定义了异常和错误处理逻辑,在错误处理逻辑中抛出异常,在异常处理逻辑中触发错误

set_exception_handler(function(Exception $e){
    echo 'catch exception:' . $e->getMessage() . PHP_EOL;
    trigger_error('error in exception handler');
});
set_error_handler(function($errno, $errstr){
    echo 'catch error:' . $errstr . PHP_EOL;
    throw new Exception('exception in error handler');
});

外部触发了错误

trigger_error('origin error');

输出:

catch error:origin error

推酷是个无耻的网站

catch exception:exception in error handler异常

catch error:error in exception handlerPHP的错误和异常处理总结

PHP Fatal error: Uncaught exception ‘Exception’ with message ‘exception in error handler’ in /tmp/error.php:9推酷是个无耻的网站

Stack trace:

leo108's blog

0 [internal function]: {closure}(1024, ‘error in except…’, ‘/tmp/error.php’, 5, Array)

1 /tmp/error.php(5): triggererror(‘error in except…’)

2 [internal function]: {closure}(Object(Exception))

3 [main]

thrown in /tmp/error.php on line 9

结论:调用了两次错误处理逻辑,一次异常处理逻辑。

推酷是个无耻的网站

外部抛出异常

throw new Exception('origin exception');

输出:

catch exception:origin exception

catch error:error in exception handlerPHP的错误和异常处理总结

PHP Fatal error: Uncaught exception ‘Exception’ with message ‘exception in error handler’ in /tmp/error.php:9

Stack trace:

推酷是个无耻的网站

0 [internal function]: {closure}(1024, ‘error in except…’, ‘/tmp/error.php’, 5, Array)

1 /tmp/error.php(5): triggererror(‘error in except…’)

2 [internal function]: {closure}(Object(Exception))

3 [main]

thrown in /tmp/error.php on line 9

leo108's blog

结论:异常处理逻辑和错误处理逻辑各被调用一次。

Centos下使用mock构建rpm

在centos下执行rpmbuild -ba package.spec的时候,通常会要求系统也安装对应的依赖包,这样就会导致系统装上许多没用的软件包,占用空间。于是搜索了一下,发现可以通过mock命令来解决这个问题。

leo108's blog

首先是安装mock,由于mock是在epel仓库里的,所以还需要先装epel仓库

yum -y install epel-release
yum -y install mock

通常情况下使用rpmbuild会新开一个用户,比如builder,这样就不会污染系统环境。我们需要把builder用户加入mock用户组

rpm

usermod -a -G mock builder

mock下使用rebuild需要src.rpm文件,所以如果只有.spec文件的话,需要先生成src.rpm文件http://leo108.com

rpmbuild -bs package.spec

这样在SRPM目录下就会生成一个src.rpm文件了,然后就可以通过mock命令来rebuild rpm文件

首先需要初始化mock环境,在/etc/mock文件夹下有各个环境的配置文件,比如centos 6就是epel-6-x86_64,初始化命令就是:

mock

mock -r epel-6-x86_64 --init

初始化完毕之后就可以开始构建了rpm

mock -r epel-6-x86_64 rebuild package-1.1-1.src.rpm

构建完毕,rpm文件会存放在/var/lib/mock/epel-6-x86_64/result目录下。当然我们可以通过–resultdir参数来指定rpm文件的生成目录rpmbuild

mock -r epel-6-x86_64 rebuild package-1.1-1.src.rpm --resultdir=/home/builder/rpms

最后执行clean命令清理环境推酷是个无耻的网站

mock -r epel-6-x86_64 --clean

 rpm

Symfony支持多个站点(应用)

对于一个网页系统来说,通常会需要面向用户的站点和面向管理员的站点,有的甚至还需要面向App的api站点。这些站点拥有不同的域名,但却共享核心业务逻辑。

Symfony的标准发行版只支持一个站点,虽然可以通过路由系统中的Host配置,根据不同的域名使用不同的路由规则,这样也可以实现类似多站点的功能,但缺点也非常明显:http://leo108.com

  • 如果希望某个Service在不同的站点有不同的表现,就没办法实现(DI不能直接注入Request)。
  • 静态文件没办法很好拆分开来
  • 每个页面请求都需要加载所有站点的配置(bundle、路由规则、Service等等),影响性能
  • 不同的站点的异常处理逻辑不同(例如对于NotFoundHttpException,在Api站点可能需要输出一个json串而在网页端需要输出一个404页面)

经过搜索,发现也有人有相同的困惑,也给出了一个初步的解决方案。但是还是有一些细节方面的问题,比如标准发行版自带的Composer post-install-cmd/post-update-cmd(清文件缓存、生成bootstrap.cache.php、发布静态文件到web根目录等)不能正常使用。那篇文章只是通过软链解决了bootstrap.cache.php的问题,但并没有提到清文件缓存等。

这个问题只能自己写代码来解决了,新建一个composer项目,依赖于sensio/distribution-bundle,新建一个ScriptHandler类,代码如下:

http://leo108.com

namespace Dreamore\DreamoreBundle\Composer;

use Composer\Script\CommandEvent;
use Sensio\Bundle\DistributionBundle\Composer\ScriptHandler as Base;

class ScriptHandler extends Base
{
    /**
     * Composer variables are declared static so that an event could update
     * a composer.json and set new options, making them immediately available
     * to forthcoming listeners.
     */
    protected static $options = array(
        'dm-apps'           => array(),
        'dm-assets-install' => 'hard',
        'dm-cache-warmup'   => false,
    );

    /**
     * Builds the bootstrap file.
     *
     * The bootstrap file contains PHP file that are always needed by the application.
     * It speeds up the application bootstrapping.
     *
     * @param $event CommandEvent A instance
     */
    public static function buildBootstrap(CommandEvent $event)
    {
        $options = static::getOptions($event);
        foreach ($options['dm-apps'] as $config) {
            $bootstrapDir = $config['app-dir'];
            $autoloadDir  = $config['autoload-dir'];

            if (!static::hasDirectory($event, 'app-dir', $bootstrapDir, 'build bootstrap file')) {
                return;
            }

            if (!static::hasDirectory($event, 'autoload-dir', $autoloadDir, 'build bootstrap file')) {
                return;
            }

            static::executeBuildBootstrap($event, $bootstrapDir, $autoloadDir, $options['process-timeout']);
        }
    }

    /**
     * Clears the Symfony cache.
     *
     * @param $event CommandEvent A instance
     */
    public static function clearCache(CommandEvent $event)
    {
        $options = static::getOptions($event);
        foreach ($options['dm-apps'] as $config) {
            $consoleDir = $config['app-dir'];
            if (!static::hasDirectory($event, 'app-dir', $consoleDir, 'execute command')) {
                return;
            }

            $warmup = '';
            if (!$options['dm-cache-warmup']) {
                $warmup = ' --no-warmup';
            }

            static::executeCommand($event, $consoleDir, 'cache:clear'.$warmup, $options['process-timeout']);
        }
    }

    /**
     * Installs the assets under the web root directory.
     *
     * For better interoperability, assets are copied instead of symlinked by default.
     *
     * Even if symlinks work on Windows, this is only true on Windows Vista and later,
     * but then, only when running the console with admin rights or when disabling the
     * strict user permission checks (which can be done on Windows 7 but not on Windows
     * Vista).
     *
     * @param $event CommandEvent A instance
     */
    public static function installAssets(CommandEvent $event)
    {
        $options = static::getOptions($event);
        foreach ($options['dm-apps'] as $config) {
            $needAssets = isset($config['need-assets']) ? $config['need-assets'] : true;
            if (!$needAssets) {
                continue;
            }

            $consoleDir = $config['app-dir'];
            if (!static::hasDirectory($event, 'app-dir', $consoleDir, 'execute command')) {
                return;
            }

            $webDir = $config['web-dir'];

            $symlink = '';
            if ($options['dm-assets-install'] == 'symlink') {
                $symlink = '--symlink ';
            } elseif ($options['dm-assets-install'] == 'relative') {
                $symlink = '--symlink --relative ';
            }

            if (!static::hasDirectory($event, 'web-dir', $webDir, 'install assets')) {
                return;
            }

            static::executeCommand($event, $consoleDir, 'assets:install '.$symlink.escapeshellarg($webDir), $options['process-timeout']);
        }

    }

    protected static function getOptions(CommandEvent $event)
    {
        $options = array_merge(static::$options, $event->getComposer()->getPackage()->getExtra());
        $options['process-timeout'] = $event->getComposer()->getConfig()->get('process-timeout');

        return $options;
    }
}

这里就重写buildBootstrap\clearCache\installAssets这三个方法的逻辑,同时为了避免冲突,我重新命名了配置项,composer.json的配置如下:leo108's blog

"autoload": {
    "psr-4": {
        "": "src/"
    },
    "files": [
        "apps/api/ApiKernel.php",
        "apps/admin/AdminKernel.php",
        "apps/wap/WapKernel.php"
    ]
},
"scripts": {
    "post-install-cmd": [
        "Dreamore\\DreamoreBundle\\Composer\\ScriptHandler::buildBootstrap",
        "Dreamore\\DreamoreBundle\\Composer\\ScriptHandler::clearCache",
        "Dreamore\\DreamoreBundle\\Composer\\ScriptHandler::installAssets"
    ],
    "post-update-cmd": [
        "Dreamore\\DreamoreBundle\\Composer\\ScriptHandler::buildBootstrap",
        "Dreamore\\DreamoreBundle\\Composer\\ScriptHandler::clearCache",
        "Dreamore\\DreamoreBundle\\Composer\\ScriptHandler::installAssets"
    ]
},
"extra": {
    "dm-apps": [
        {
            "app-dir": "apps/api",
            "autoload-dir": "apps",
            "need-assets": false
        },
        {
            "app-dir": "apps/admin",
            "autoload-dir": "apps",
            "web-dir": "web/admin"
        },
        {
            "app-dir": "apps/wap",
            "autoload-dir": "apps",
            "web-dir": "web/wap"
        }
    ],
    "dm-assets-install": "relative"
}

autoload中的file加入各个站点的kernel文件,这样就不需要手动require了。推酷是个无耻的网站

scripts替换成我们自己的ScriptHandler

http://leo108.com/pid-2202.asp

dm-apps是一个数组,每个站点一项,每个站点的配置有app-dir、autoload-dir、web-dir、need-assets,app-dir代表kernel文件所在的目录;autoload-dir代表autoload.php文件所在的目录,由于各个站点的autoload.php完全一致,所以我就把这个文件放到apps目录下,所有站点共享(因此需要修改每个站点的app.php app_dev.php和console这3个文件);web-dir代表发布静态文件的目标目录;need-assets代表是否需要发布静态文件(比如api这个站点就不需要发布静态文件)。http://leo108.com/pid-2202.asp

这样每次composer update或者composer install之后,我们的ScriptHandler就会对每一个站点执行生成bootstrap文件、清理文件缓存和发布静态文件的操作。

专注于技术,切不可沉湎于技术