MySQL Connection Error: (2002) Connection refused

最近一直有事没看博客,直到昨晚Google给发了封邮件,说检测到服务器上的错误呈现增加趋势

乘今天有点空闲,上了服务器看看。

发现wordpress和mysql这两个docker container都是正常运行,没挂掉。

按照以前的套路

docker restart mysql
docker restart wordpress

结果不行,看来不是小问题了。

针对这两个docker分别用docker logs命令查看了日志,mysql的没发现什么比较明显的错误报告,但是wordpress里面就满屏都是了

Warning: mysqli::mysqli(): (HY000/2002): Connection refused in - on line 10

MySQL Connection Error: (2002) Connection refused

上网找了半天,发现有人说可以通过重启docker来修复

试了一下,我的服务器是CentOS的,命令如下:

service docker stop
service docker start

重启完了之后,发现wordpress的container停掉了,用docker start wordpress给启用了

再次刷新网站,恢复正常了!

博客搬迁2017

起因

18+1大,因为作死的还一直开着我的ss,然后连同着博客一起被墙了。

想上服务器做一些修改,试过各种ssh的代理方法,就是不行。在调用这些手段的时候,突然发现有人提到VPS可以做到月费5$,有点惊呆了。https://www.diycode.cc/topics/738

仔细想想,自己现在这个EC2,每个月开销已经到$10了,其实自己也没怎么用,主要就是挂博客和放代理,那还不如索性乘这个机会迁到更符合实际需求单VPS上。

目标

这次迁站,主要两件事情,一个是搭建ss-server,一个就是wordpress和mysql数据的迁移。

wordpress和mysql都是基于docker部署的,算是其中的一个小难点

先在vultr.com上面创建了instance,等安装好了之后,用给的帐号密码登录指定服务器,成功。

ss-server搭建

使用上面说的文章梨提供的一键安装脚本,ss-server顺利安装成功。

在本地建好配置文件(config.json)之后,使用pip3 install shadowsocks快速安装了客户端,然后ss-local -c config.json启动

可是一直连不上,ss-local里面的日志都是connecting

在网上找了一圈之后,发现可能是本机没开放对应端口,使用命令:

 firewall-cmd --zone=public --list-ports

查看了一下现有开放的端口,发现ss默认使用的8989是开放的,不知道是为啥还被禁。

最后的解决方式是:kill掉ss-server, 修改配置文件,更换端口到8083(随便选的),然后启动ss-server。在防火墙这块:

firewall-cmd --zone=public --add-port=8083/tcp
firewall-cmd --reload

再尝试,可以顺利链接了。

代理配置好之后,我本地也是能方便的使用Google,为接下来查看怎么迁移Docker做了准备。

上面关于firewall-cmd相关的知识,是来自: http://www.111cn.net/sys/CentOS/103509.htm

迁移站点

因为vultr准备的这个instance是一个最小的CentOS安装包,所以得自己使用yum安装了vim和nginx:

yum install vim
yum install nginx

安装好之后,直接IP访问,发现不可以,一看还是防火墙的问题,解决方法跟上面一样:

firewall-cmd --zone=public --add-port=80/tcp
firewall-cmd --reload

关于docker的迁移,我一开始主要参照这篇文章:Docker 容器迁移,可真按照这个完成之后,得到的是一个完全新的博客,能顺利运行,但是之前的数据都没了。

无奈之下,我又单独去了解了一下Docker Mysql的数据迁移,主要参照的这篇文章:Docker Mysql容器间数据简单迁移。这次就靠谱多了,同时还学会了怎么在主机和docker之间互相拷贝文件。

在顺利从docker里面导出sql文件到主机后,再使用scp命令在vultr和aws之间传递文件(忘记说了,登录上vultr之后,我成功的基于vultr的主机ssh上了aws的主机,机智)

scp命令如果遇到错误: Permission denied,可以参照这篇文章: ssh使用scp: /目录: Permission denied

不过这篇文章在目标数据库恢复数据这块,说的不够清楚,用文章里面的方法始终没办法在mysql里面出现一个叫wordpress的数据库。

继续google,找到了一个在sql文件导入导出方面比较专业的文章: linux命令行下导出导入.sql文件,原来导入之前是得先新建对应的数据库,在进入数据库之后,再执行source data.sql就能顺利的导入了。

数据库导入之后,再查看网站,已经能正确展示了,不过一些图片资源报了404,看了看资源路径,在wp-content目录里面。

docker exec进入wordpress所在的docker里面,看了一下wp-content目录,空的,看来跟mysql一样,也是迁移的有问题的。

用了之前从mysql的docker里面导出sql文件的方法,从旧网站的docker里面把wp-content目录给导出来,然后scp下载到vultr主机,再复制到新网站的docker里面(好累),资源顺利加载。

不过一些插件报错了:**Call to undefined function wp_get_upload_dir()
** 
这种错误,我是不想去debug了,很明显,光拷贝wp-content是不行的,索性,那就完全贝把。把docker里面的/var/www/html/完全拷贝出来,再覆盖现有的。覆盖的时候,是打算

mv html hml-bk
tar zxf html.tar.gz

这样来做的,不过不让直接重命名现有的html目录,提示 mv: cannot move ‘html’ to ‘html-bk’: Device or resource busy

那就只好先新建个文件把旧网站的html目录放进去,然后一个个文件夹的替换。替换完成之后。

运气不错,现在新网站顺利运行了。

总结

这次的站点迁移,跟想像的优雅略有差别,以为用上Docker之后,这种迁移会是很轻松的一件事情,却没想到折腾了这么久。

应该是我自己在Docker这块了解的还是太少吧,等以后有时间再好好看看这块。

对了,顺便放上我的Vultr的推广链接:https://www.vultr.com/?ref=7256603,有需要VPS的朋友可以考虑一下点我的链接去注册哈,在此谢谢了

React Native错误: “:CFBundleIdentifier”, Does Not Exist

最近项目需要,决定入手React Native。

第一步要做的肯定就是能在本机顺利的把官网的Quick Start 跑出来.

先说一下本机的环境:

  • 系统: macOS Sierra 10.12.5
  • NodeJs: 8.1.0
  • Xcode: 8.3.3 (8E3004b)
  • react-native-cli: 2.0.1
  • react-native: 0.45.1

构建过程

官网是用的brew来安装node,但是我这边根据日常开发的需要,会在多个Node版本之间切换,所以我是用的nvm

接下来是watchmanreact-native-cli,都是按照教程来一步一步安装

brew install watchman
npm install -g react-native-cli

然后是初始化和启动项目

react-native init AwesomeProject
cd AwesomeProject
react-native run-ios

然后就出错了:“:CFBundleIdentifier”, Does Not Exist

debug过程

先是在github上面找到了一个同样错误的issue,发现有这个错误的人很多,给的解释也不少

端口占用

端口8081被别的程序占用了

lsof -n -i4TCP:8081
node    7601 CXI624   23u  IPv6 0xc6b249599e5f1169      0t0  TCP *:sunproxyadmin (LISTEN)
kill - 9 7601 

这里面的7601只是一种情况,具体看真实占用这个端口的是什么程序。

我这边可以确定不是,所以pass

重买一台mac解决了

这… 没钱,pass

react-native upgrade

更新react-native到最新版

我这刚刚下载的,肯定最新版没毛病

sudo

sudo react-native run-ios

这个我是不太想用的,因为用了之后,以后基本都得用sudo,比较麻烦,等确认没别的解决方式再考虑

尝试调整Xcode的配置:

  1. Go to File -> Project settings
  2. Click the Advanced button
  3. Select “Custom” and select “Relative to Workspace” in the pull down
  4. click done, done

按照上面的做了一下,然后直接基于Xcode来运行程序

结果又出了新的问题

double-conversion.build failed

然后是在这个issue下找到了问题的解决方式:

rm -r ~/.rncache
rm -r <your-project>/node_modules/react-native/third_party

不过最终的编译还是失败。

网络问题

最后在刚刚的那个issue里面找到了那么一段话:

The problem is when building react-native for iOS, the process tries to download some files (using curl) into /.rncache. Specifically, they are “boost1630.tar.gz, double-conversion-1.1.5.tar.gz folly-2016.09.26.00.tar.gz, glog-0.3.4.tar.gz”. The sources of them (at least some of them) are from AWS S3, which aren’t accessible in some regions (I’m from China). And that caused the problem.
By using a proxy server and setting the http_proxy and https_proxy environment variables, curl will go through the proxy and the build should succeed.
This is not a problem with React Native. This issue can be closed now.

是说的在build的时候,会从AWS S3里面下载一些资源,但是在咱们国内,很多时候是访问不到这些资源的,也自然就导致了一些文件的缺损。

作者在后面给的方式是给curl配置代理,具体看https://stackoverflow.com/questions/9445489/linux-curl-command-with-proxy

不过我这边用了另外一种方式,把我的蓝灯设置为代理全部流量,接着

rm -r ~/.rncache
rm -r <your-project>/node_modules/

把所有资源删掉,然后重新编译。

最后,成功编译通过!!

python错误:module ‘shutil’ has no attribute ‘copy’

由一个AttributeError引发的一系列探索

字符:5706
平移阅读时长:7分钟

今天在写一个小脚本来把自己写的一个html批量复制并在保持后缀不变的情况下随机命名。

先打算实现文件的拷贝。

Google了一下,发现了一个shutil的库(这个库的名字纠结了发音老半天,后面才理解是sh和util的结合,也就是shell-util)

这个库提供了与shell命令操作文件等同的所有方法:copy, copytree, rmtree, move

代码实现

有了这个库,代码很容易就实现了:

import shutil

src = 'sa-jumper.html'

randomStr = 'ad239sdfasdl2asdf9adsfi23dfladf'
strLen = len(randomStr)

shutil.copy(src, 'tmp/aa.html')

关于随机的部分,就只是提前做了准备。先用的shutil.copy来测试文件拷贝功能。

然后我先用的python3.6执行了这个文件,结果报错:

Traceback (most recent call last):
  File "copy.py", line 2, in <module>
    import shutil
  File "/usr/local/Cellar/python3/3.6.0_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/shutil.py", line 13, in <module>
    import tarfile
  File "/usr/local/Cellar/python3/3.6.0_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/tarfile.py", line 49, in <module>
    import copy
  File "/Library/WebServer/Documents/xxx/copy.py", line 13, in <module>
    shutil.copy(src, 'tmp/aa.html')
AttributeError: module 'shutil' has no attribute 'copy'

后面改用python2.7执行,顺利通过。

debug

打开Python的解释器提示符,执行上述代码,也是成功的。

这下我就没辙了,感觉个人的Python经验只能到此为止,启用Google大法。

在StackOverflow的一篇文章上,发现有人遇到类似的问题,链接如下:

https://stackoverflow.com/questions/22131139/attributeerror-module-object-has-no-attribute-x

在上述链接里面,最终的错误是因为提问的人,python脚本的名称是org.py跟系统的一个文件重名,导致了一个错误的引用。

所以很自然的,我就联想到了我的文件,名字是copy.py,而看上面的错误信息里面,是用一个import copy,估计也是同样错误的把我的脚本当作官方库给错误引用了。

修复

把文件名改成了copy-file.py,再用python3.6执行,一切顺利。

分析

从这个错误里面想到几个问题:

  • 为啥跟官方库里面的文件同名会导致引用错误
  • 为啥python2.7没报错
  • 如何避免这样的问题

关于引用错误

重新翻了一下之前看的《Python简明教程》

关于模块的索引

如果它不是一个已编译好的模块,即用 Python 编写的模块,那么 Python 解释器将从它的 sys.path 变量所提供的目录中进行搜索。如果找到了对应模块,则该模块中的语句将在开始运行,并能够为你所使用。在这里需要注意的是,初始化工作只需在我们第一次导入模块时完成。

关于索引的顺序

sys.path 内包含了导入模块的字典名称列表。你能观察到 sys.path 的第一段字符串是空的——这一空字符串代表当前目录也是 sys.path 的一部分,它与 PYTHONPATH 环境变量等同。这意味着你可以直接导入位于当前目录的模块。否则,你必须将你的模块放置在 sys.path 内所列出的目录中。

从这里可以看到,执行的脚本所在的目录也是在python搜索模块的范围内的,而且是第一位,也就是会优先搜索的,这也就是为啥同名会导致引用错误的原因了。

为啥2.7没错

按照上面StackOverflow那边文章的分析过程,我分别看了python3.6和python2.7版本里面shutil.pytarfile.py源码,发现shutil.py都引用了tarfile.py,而tarfile.py也都import copy

那就不可能是2.7里面没有引用copy了。

然后我又仔细的看了一下各自的实现,终于发现了问题,在2.7里面,不是在初始化的时候就import了tarfile,而是在一个_make_tarball 的函数里面。

我在这个函数的import tarfile前面加了一个print('import tarfile'),再去执行刚刚的copy.py(这块为了测试方便,把copy-file.py复制了一份重命名为copy.py)。

发现没有print任何内容。

而在3.6里面,是在文件头部,一开始就import了tarfile。这也就解释了为啥2.7没问题,3.6有问题了。同时也说明:python的import是按需加载的。写在函数里面的import,只有在函数被调用的时候才会真实的去import

为了反证这个推断是否有效,我找到了2.7里面shutil的一个会触发import tarfile的函数make_archive

使用了官方提供的example:

import shutil
import os
archive_name = os.path.expanduser(os.path.join('~', 'myarchive'))
root_dir = os.path.expanduser(os.path.join('~', '.ssh'))
shutil.make_archive(archive_name, 'gztar', root_dir)

执行python copy.py(我的环境里面,默认是2.7)

果然报错了:

import tarfile
import tarfile
Traceback (most recent call last):
  File "copy.py", line 20, in <module>
    shutil.make_archive(archive_name, 'gztar', root_dir)
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 561, in make_archive
    filename = func(base_name, base_dir, **kwargs)
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 376, in _make_tarball
    import tarfile  # late import so Python build itself doesn't break
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/tarfile.py", line 52, in <module>
    import copy
  File "/Library/WebServer/Documents/xxx/copy.py", line 20, in <module>
    shutil.make_archive(archive_name, 'gztar', root_dir)
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 561, in make_archive
    filename = func(base_name, base_dir, **kwargs)
  File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 394, in _make_tarball
    tar = tarfile.open(archive_name, 'w|%s' % tar_compression[compress])
AttributeError: 'module' object has no attribute 'open'

然后我尝试把文件名修改为copy-file.py,再执行,顺利通过。

看来2.7和3.6都是一样的索引模块的方式,只是因为shutil模块里面引用tarfile的方式不一样,才造成一开始的执行结果不一样。

如何避免这样的问题

我还是一个python新手,不知道python在命名的时候有没有提供一些合理的规范,比如不能使用的单词。

然后我又一次看了《简明Python教程》,里面关于标识符命名这块:

量是标识符的一个例子。标识符(Identifiers) 是为 某些东西 提供的给定名称。在你命名标识符时,你需要遵守以下规则:
• 第一个字符必须是字母表中的字母(大写 ASCII 字符或小写 ASCII 字符或 Unicode 字符)或下划线()。
• 标识符的其它部分可以由字符(大写 ASCII 字符或小写 ASCII 字符或 Unicode 字符)、下划线(
)、数字(0~9)组成。
• 标识符名称区分大小写。例如,myname 和 myName 并不等同。要注意到前者是小写字母 n 而后者是大写字母 N。
• 有效 的标识符名称可以是 i 或 name_2_3 ,无效 的标识符名称可能是 2things,this is spaced out,my-name 和 >a1b2_c3。

我想,如果我以非这个规范的方式定义文件名是不是就可以了。

然而用类似import copy-file的方式引用自己的模块,会报错,SyntaxError

也有看到有相关的解决方式:

  • python2.7 execfile('foo-bar.py')
  • python3.x exec(open(fn).read())

不过这些方法感觉都不是很自然,还是放弃这样的尝试了。

只能使用另一种方式了,就是统一文件名的前缀。如果你的项目是foo,那么可以给所有项目的文件定义以这个为前缀的名称: foo__copy.py, foo_shutil.py等等。

这样只要foo是一个不会和常见的库的前缀重名的字符串,就能保证项目里面不会再发生这样的名称冲突了。

参考资料

突然发现,我只是想实现一个文件拷贝,咋就绕了这么远呢?

在Vim里面列出TODO和FIXME等备注

很多程序员,包括我,会在写代码的时候,习惯性的用TODO, FIXME, HACK等作为一些注释的前缀。这样方便自己和后来者预先知道相关注释和代码的状况

不过这样的备注只有是以后恰巧看到这段代码的时候才能看到,没法在一次发版或者空闲的时候,有一个直观的界面能看到这个项目里面所有这样的关键词。

这个时候一个简单的自定义命令兴许能帮到你:

command Todo Ack! 'TODO\|FIXME'

效果如下:

不过要想使这条命令生效,需要一些环境准备工作

Ack

首先我们需要安装Ack或者Ag,这两个都是命令行下的一个全局搜索工具,比系统自带的grep要快很多的。

这里的安装以Ag为例,因为这个是基于C来重写的Ack并新增了很多特性,效率也是比Ack快了很多的。

# OSX
brew install the_silver_searcher
# Archlinux
pacman -S the_silver_searcher
# Ubuntu
apt-get install silversearcher-ag

在安装完成之后,就可以在shell里面测试一下

ag TODO ./

Ack.vim

Ack.vim是一款插件,用来方便在Vim里面使用Ack/Ag, 然后通过Quickfix来提供搜索结果

不过这款插件,默认是使用的Ack作为搜索引擎,为了能用上我们刚刚安装的Ag,需要在~/.vimrc里面作一些配置:

if executable('ag')
  let g:ackprg = 'ag --vimgrep'
endif

这段配置是引用的官方仓库的Readme

不过也看很多人是用的

if executable('ag')
  let g:ackprg = 'ag --nogroup --nocolor --column'
endif

本人测试之后,发现二者并没有什么区别。可能对于Ag来说 --vimgrep是等同于--nogroup --nocolor --column 的。

这个时候在Vim的命令行里面输入

:Ack! TODO 

是可以实现上图所示效果的,而且还可以通过

:AckAdd FIXME

这样的方式来附加其它要查看的。

不过这样的方式,命令还是有点长。所以配置一个alias来替代上面的命令,是一个比较符合懒人的想法

~/.vimrc里面新增

command Todo Ack! 'TODO\|FIXME'

然后在Vim的命令行里面输入:

:Todo

就能实现列出当前项目里面的所有TODO,FIXME了,而且可以根据自己的需要来扩展这样的关键字,像我就扩展为如下代码:

command Todo Ack! 'TODO|FIXME|CHANGED|BUG|HACK'
command Debug Ack! 'NOTE|INFO|IDEA'

区分了Todo和Debug两种功能。

高亮

Vim里面会自动的高亮TODOFIXME,但是不会高亮CHANGED这样的关键字,所以需要手动的来指定一下,在~/.vimrc里面添加如下配置:

if has("autocmd")
  " Highlight TODO, FIXME, NOTE, etc.
  if v:version > 701
    autocmd Syntax * call matchadd('Todo',  '\W\zs\(TODO\|FIXME\|CHANGED\|BUG\|HACK\)')
    autocmd Syntax * call matchadd('Debug', '\W\zs\(NOTE\|INFO\|IDEA\)')
  endif
endif

上面这段代码是摘抄的,具体的功能就是匹配TODO等关键字,然后加上语法高亮。

综上,一个简单好用的TODO管理器就已经完成了。

参考资料

上文里面的绝大部分代码都是从以下资料里面摘抄拼凑的,对于Vim,我还只是一个懵懵懂懂的拿来主义者。

  1. https://coderwall.com/p/prfnnw/vim-to-do-list
  2. https://stackoverflow.com/questions/6577579/task-tags-in-vim
  3. http://harttle.com/2015/12/21/vim-search.html
  4. https://github.com/mileszs/ack.vim
  5. http://betterthanack.com/

用Python实现FTP文件上传

前情

最近在做活动编辑器的时候,考虑到运营人员很多都没有图片上传的权限,所以想在活动编辑器里面内置一个这样的功能,让运营可以使用上传控件或者托拽的方式上传图片到服务器,并在上传成功之后,自动把地址放到文本框里。

第一步要做的就是尝试着实现用FTP来上传一个文件到服务器的指定目录,同时基于文件的sha1值来命名上传之后的文件。

手上现有的两种实现路径是NodeJsPython,不过想到最近刚学Python,正需要这样一个机会,就先尝试python的方式。

开发

先是Google了一下,知道了一个ftplib的内置模块可以用来做FTP相关的功能。然后是之前在即刻上有看到余弦发的一个文章,说到用Python一句话查看文件的哈希值,上去找了找,也很快找到了那篇文章里面的相关代码。

折腾一下之后,实现代码如下:

import ftplib
import hashlib
import os

host = 'YOUR HOST'
username = 'YOUR NAME'
pwd = 'YOUR PWD'
port = 21
filePath = 'some/path/to/img.jpg'
serverPath = '/where/to/store/your/file/on/the/server/'

session = ftplib.FTP()

session.connect(host, port)

session.login(username, pwd)

session.cwd(serverPath)

file = open(filePath, 'rb')

filename, file_extension = os.path.splitext(filePath)

str_hash = hashlib.sha1(file.read()).hexdigest()

target_file_name = str_hash[:16]+file_extension

session.storbinary('STOR '+target_file_name, file)

file.close()

session.quit()

代码写完,就顺手执行看了下效果:文件顺利按的传到了预设的目录,也重命名为了指定的文件名。但是,文件的体积是0

调试

在Python的IDEL环境里面使用help命令查看了file相关的各种文档,最后把问题的方向指向了file.read。因为当时在IDEL环境里面,使用file.read读取了要上传的文件内容之后,再次调用file.read时,返回的内容为空。

于是在Google上面用python file read twice作为搜索词搜索了一下。果然找到了问题,文件里面有一个指针指向上一次读取的位置,默认是在最开始,也就是0。file.read会读取文件的全部内容,完成之后,指针就指向文件的最末尾。

所以,在调用过一次file.read之后,再次调用file.read,会从文件最末尾开始获取内容,这获取的内容自然就是空了。而ftp.storbinary在上传文件的时候,内部应该也是会使用类似file.read类似的方法来获取要上传的文件内容并传递给服务器。这个时候也自然就没法正常获取正确的文件内容

解决方式是在file.read之后,使用file.seek(0)来把文件的指针指回最开始。

最终代码:

import ftplib
import hashlib
import os

host = 'YOUR HOST'
username = 'YOUR NAME'
pwd = 'YOUR PWD'
port = 21
filePath = 'some/path/to/img.jpg'
serverPath = '/where/to/store/your/file/on/the/server/'

session = ftplib.FTP()

session.connect(host, port)

session.login(username, pwd)

session.cwd(serverPath)

file = open(filePath, 'rb')

filename, file_extension = os.path.splitext(filePath)

str_hash = hashlib.sha1(file.read()).hexdigest()

target_file_name = str_hash[:16]+file_extension

# 前面的file.read使得file的read cursor指向了文件的尾部
# 如果不手动调整指针的话,后面的文件上传,就会是一个空文件
file.seek(0)

session.storbinary('STOR '+target_file_name, file)

file.close()

session.quit()

参考文档

  1. Python Script Uploading files via FTP
  2. Extracting extension from filename in Python
  3. Why can’t I call read() twice on an open file?
  4. 我是如何 Python 一句话校验软件哈希值的

Docer for Mac的ca.pem找不到的问题

之前安装过Docker Toolbox来实现在Mac上面跑Docker,后来Docker又出了一个Docker for Mac,好奇心一起就卸了Docker Toolbox,装了它。

愿意尝试的原因是Docker Toolbox主要是用VirtualBox来放container,Mac环境里面相关的docker命令其实都是代理,最终的实现是在VirtualBox里面。而Docker for Mac用的是macOS的一个框架HyperKit来实现的,不需要使用VirtualBox来做中间代理,性能上会有很大的提升。当然,前提是你的系统是升级到了macOS10.11。

顺利安装和启动Docker for Mac之后,在命令行运行docker info命令,会出现如下错误。

could not read CA certificate "/Users/{{username}}/.docker/ca.pem": open /Users/{{username}}/.docker/ca.pem: no such file or directory

在网上找了不少解决方式,

bash_profile

尝试了下这个网址里面说的https://github.com/boot2docker/osx-installer/issues/126
我的用户目录下没有.bash_profile的文件,我是用的zsh,所以在.zsh_rc找了找(安装Docker Toolbox是很久之前的事情了,根本不记得当初怎么安装的),意外的也找到了当初配置的一些DOCKER_*的环境变量。注释,重启zsh,再次运行docker info,还是不行。

docker machine

然后又继续尝试上面网址里面,后续给的方法:

docker-machine regenerate-certs default

 egenerate TLS machine certs?  Warning: this is irreversible. (y/n): y
 Regenerating TLS certificates
 Host does not exist: "default"

失败。

既然是说default不存在,那我就来创建一个

查看了docker-machine相关的命令,刚好有一个create命令,执行了一下:

docker-machine create default
 Creating CA: /Users/{{username}}/.docker/machine/certs/ca.pem
 Creating client certificate: /Users/{{username}}/.docker/machine/certs/cert.pem
 Running pre-create checks...
 Error with pre-create check: "exit status 126"

搜了一下关于exit status 126 的相关错误,说的是没安装VirtualBox,这时候我觉得不对劲了,因为跑Docker for Mac是不需要VirtualBox的啊。

正式看了一下docker machine是什么东西,原来是一个用来方便的管理多种类型的docker主机的一个工具,可以是虚拟机,本地主机和云平台,具体可以看看这个链接:https://yeasy.gitbooks.io/docker_practice/content/machine/usage.html

顺便推荐一下这本电子书《Docker —— 从入门到实践》

看完docker machine的作用,可以看的出来,这和我现在想做的事情和遇到的问题,没有太大干系。我是安装完了Docker的环境,然后在执行docker命令的时候出现了问题,不是在已经有container的时候在维护上出了问题。

环境变量

这时候我开始回到官网来追根溯源了,很幸运的找到一篇深入比较Docker for Mac和Docker Toolbox的文章:Docker for Mac vs. Docker Toolbox

这个对于Docker for Mac和Docker Toolbox各自的实现机制说的十分透彻,同时还意外的有一个Setting up to run Docker for Mac章节,说了如何配置Docker for Mac的环境,在你已经或者曾经安装过Docker Toolbox的情况下。

按照上面说的执行命令

env | grep DOCKER

顺利的找到了所有的DOCKER_*的环境变量

对对应的变量执行 unset

 unset DOCKER_TLS_VERIFY
 unset DOCKER_CERT_PATH
 unset DOCKER_MACHINE_NAME
 unset DOCKER_HOST

再次执行

env | grep DOCKER

保证环境变量已经清除。

然后执行

docker info

一个正常的信息列表出来了

Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 17.03.1-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host ipvlan macvlan null overlay
Swarm: inactive
Runtimes: runc
Default Runtime: runc
...

以上就是整个debug的过程。其实这么折腾,主要还是自己不是很了解docker,得抽空再好好看看上面说的那本电子书才行了。

QQ内置浏览器下的QQ登录实现

在PC时代,调用QQ, 微博之类的三方登录,对于前端来说是比较轻松的(如果不需要异步登录的话),而且体验也不错,QQ那边可以自动识别是否登录,对于用户只是一个确认和点击的流程。

但是到了现在,移动互联的时代,这样的三方登录完全都是APP那边前端的事情,Web前端无能为力。毕竟,让用户在一个小设备上的小网页里面输入他已经在登录状态的QQ的账号密码,是挺难为人也挺不现实的。

缘于产品庆庆同学的执着,偶然发现在QQ内置的浏览器里面进行QQ登录的话,是也可以跟PC那样自动识别到已登录状态的,这让事情有了一线曙光。不过这种便利也只能在QQ的内置浏览器里面

下面就来说说如何实现web网页在QQ内置浏览器里面的登录

识别QQ的内置浏览器

首先得识别当前所在的浏览器是不是QQ内置的。

QQ浏览器分两种情况,一个是独立的浏览器产品,一个是内置在QQ里面的webview。识别的方式,用userAgent。

先贴出对应平台对应分类的UA(使用v2ex提供的一个工具页面可以方便的打印UA )

安卓平台下的独立的QQ浏览器

Mozilla/5.0 (Linux; U; Android 4.4.4; zh-cn; HM 1S Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 Chrome/37.0.0.0 MQQBrowser/7.4 Mobile Safari/537.36

安卓平台下的内置的QQ浏览器

Mozilla/5.0 (Linux; Android 4.4.4; HM 1S Build/KTU84P; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043124 Safari/537.36 V1_AND_SQ_6.7.1_500_YYB_D QQ/6.7.1.3105 NetType/WIFI WebP/0.3.0 Pixel/720

IOS平台下的独立的QQ浏览器

Mozilla/5.0 (iPhone 5SGLOBAL; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 MQQBrowser/7.4 Mobile/14E304 Safari/8536.25 MttCustomUA/2 QBWebViewType/1

IOS平台下内置的QQ浏览器(TIM内)

Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Mobile/14E304 QQ/6.5.5.0 TIM/1.0.5.9 V1_IPH_SQ_6.5.5_1_TIM_D Pixel/640 Core/UIWebView NetType/4G QBWebViewType/1

这里因为测试设备有限,没法确切知道所有版本的QQ下的情况,只能等以后慢慢完善。

从上面的UA可以找到两个关键字QQMQQBrowser。本来两个关键字是分开的,独立浏览器里面是MQQBrowser,内置的是QQ,但是这在安卓的内置里面同时实现了。

所以判断方式需要绕一点:先判断MQQBrowser,如果存在,判断QQ是否存在,存在则是内置,不存在则是独立;如果不存在MQQBrowser,则判断QQ是否存在,存在是内置,不存在,就都不是。

不过我们这次的需求只需要判断内置,所以简单,判断QQ是否存在即可:navigator.userAgent.match(/QQ\/([\d.]+)/)

登录的实现

一开始是打算继续用PC时代用的那个登录SDK好实现一个优雅的异步免刷新的登录效果。在实际测试的时候,也的确是可以做到在登录页QQ自动识别状态只需要用户点击登录按钮确认,可是在登录回跳这块卡住了。

在PC,SDK是用window.open新开一个弹窗来实现登录和各种跳转,在新窗口的最终落地页和主窗口通信返回登录成功获取的AccessToken,然后关闭弹出窗口。

但是内置浏览器里面window.open的效果是替换当前网页展示,类似于location.href赋值跳转,这使得最终落地页一直联系不到主窗口,然后卡着了。

很自然的,就想到了用iframe,把window.open打开的地址放到iframe里面,不就可以实现两个window的通信了吗?说干就干,QQ登录界面顺利的展现了,点击登录,结果主界面跳了,跟之前一样,再次无法通信。看来QQ在登录成功之后是用window.top来跳转的。不明白QQ这样做是出于什么考虑的,安全性?可是登录成功的回调地址不是已经被限制了只能是对应域名下的了吗?唯一知道的就是,这个方法行不通……

所以,目前还是只能走以前的老路,通过页面跳转的方式来实现登录。主要的策略就是在QQ登录成功之后的回调页再跳回之前的发起登录的页面。

本来这个需求是由后端来实现,在回跳发起页之前,在cookie里面记录用户的登录信息,这样发起页就能知道用户登录成功了。

不过我这次的需求,有两个麻烦的地方:

  1. 后端目前还没有在移动站点配置登录功能
  2. 我需要知道QQ用户的OpenIDAccessToken

所以改为由后端配置好回调页路由,具体页面里面的跳转和信息记录由前端自行控制。

然后具体的实现步骤是:
1. 用户点击发起页上的QQ登录按钮
2. 前端在后端给的redirect_url里面加上一个search query字段redirect,方便后面回跳回来
3. 前端根据appIdredirect_url整合出跳转QQ登录的地址(https://graph.qq.com/oauth2.0/authorize? response_type=token&client_id={{appId}}&scope=all&display=mobile&redirect_uri={{回调地址}})并跳转
4. 在QQ登录页面,QQ识别到当前用户的登录状态,自动填充信息并添加一键登录按钮
5. 用户点击登录
6. QQ开始回跳到redirect_url并在redirect_url后附加hash query,这个query里面有 access_token expires_in
7. 这个回调页,js抓取token和token过期时间存储在localStorage,同时还在localStorage记录两个个临时的key: TempQQLoginSuccessTempQQLoginSuccessTime
8. 回调页识别search query里面的redirect字段,获取发起页地址并跳转过去。
9. 发起页初始化的时候,查看localStorage里面有没有token和expire_in对应的字段,如果没有则是未登录状态,执行一次退出操作(退出操作是尝试清空回调页记录的四个key,下文同理)。如果存在,则判断当前时间是否超过expire_in对应的时间,如果超过,则执行退出操作。没有超过,则此时用户是登录状态。
10. 确定用户登录之后,再判断回调页记录的两个临时字段是否存在。如果有,判断TempQQLoginSuccessTime存储的时间和当前时间的时间差是否有超过一定的阈值。如果没有超过则说明上一次的发起页有登录行为,可以提示登录成功,如果超过了,则直接显示登录后的状态即可。同时不管超过与否,清空这两个临时key。

以上,整个登录流程就通了,成功的获取了用户的accessToken。

后面再使用accssToken作为参数,以jsonp的方式访问QQ的接口https://graph.qq.com/oauth2.0/me?access_token={{accessToken}}就能顺利的获取到用户的openId。这里要注意的一点是,QQ的这个接口里面callback的函数名写死了是callback,所以在使用jsonp的时候,以Zepto为例,jsonpCallback需要设定为callback

关于这个项目,具体实现代码在这:https://github.com/shuizhongyueming/fe-qq-login,有兴趣的可以看看

关于我的口才

之前看曹政的书《你凭什么做好互联网》,里面关于技术人员有一个描述

大部分技术人员分享时候的盲区,自说自话,完全不考虑受众

这里摘取的不全面,不过大体意思是有的,就是很多技术人员会有的糟糕的口才,而我,正巧,也是其中一员。

不同于写作,演讲、分享、辩论,这些口语形式的思想表述,需要你在很短的时间同时掌控好思维和舌头,流畅的展现你的思想,没有编辑的概念。

而我总是想的太快,舌头跟不上,说出来的话和自己真实想表达的意思要打个折。大信息量的涌入使得舌头的性能降低,说出来的话还会有点口齿不清。

很早就意识到这个问题,可是却难有改观。

比如这次的一个活动页开发

因为业务逻辑和交互复杂的不输于一般的单页面应用,所以一改以往收集DOM操作为一个动作的抽象方式,这次用了MVVM架构的一些数据驱动视图的思路,同时还基于Redux的思想做了数据的管理。一番调整之后,自己还是有点小得意的,DOM操作和VM的完美结合。

再说点尴尬的背景,这个活动分移动和PC两个页面,功能一样,UI大体也是类似,但是却由我和同事小磊分别开发……

我知道这是一件很没脸说的事情,两个有经验的前端做了这样没动脑子的决定。虽说我是后加入这个活动开发的,可是在参与进来的时候,花一两小时好好熟悉一下项目,然后再一起讨论分工合作是一个完全合理而且必要的流程。

继续说前面的调整,因为那点小创意,所以我在开发进度上有点领先(UI和很多前期代码是直接从小磊那边拷贝过来的)。小磊那边在管理UI上的数据变动这块遇到了一些问题,业务逻辑有点复杂,各种弹窗,还有多个模块之间的数据通信,纯DOM操作对于掌控全局变化就有点吃力。

这时候为了进度,我尝试跟小磊介绍一下我的处理问题的思路。我想跟他说数据驱动,订阅者,MVVM,React,Redux……

是的,面对一个分享将会带来的荣耀,我一瞬间就想了这么多。打开我的代码,开始了介绍:“这个活动很复杂,所以咱们得用数据驱动的方式开发,你看我这个dataCenter,我把数据都放这里面用getter和action维护,然后action里面发布事件,再看这块,main这块,对数据更新做了响应。再看这块,抽奖回调里面调用action来更新数据。还不错吧,其实这个是借鉴redux里面的思想,store”。话没说完,小磊同志不耐烦了:“越说越懵,所以我这个问题该怎么解决?”

尬了一下,小磊向来耿直。

“那就先不看我这个实现,你这现在是哪有问题了”。是的,我还没弄清楚具体哪出问题了。

后面仔细看了看小磊的代码,指出了一些模版里面基于数据的逻辑判断问题,再给了一个全量更新列表模版(数据量少,就几条)的思路,总算是博得一句“哦,原来这样啊”

一点总结

伤痛中总是伴随着成长,不好好想想说出去的话,早晚是会遭殃。

在这件事上总算对自己糟糕的口才又有一次认识,也知道区分在什么场景下做技术分享,什么场景下是bugFix。做技术分享要精要细,bugFix就不要废话太多,讲究快准狠,以解决问题为只要目的。

PS:值得安慰的事第二天一上班,小磊就过来说:“我昨天晚上仔细想了想,你的那个dataCenter真是太有必要了”

Web端打开APP功能的实现

这两天公司开始发力做APP的推广,很多活动页和移动端站点需要加上打开APP的功能,如果没安装则提示用户下载。

首先,基于隐私保护的目的,浏览器是不会提供接口给网页去获取系统上安装了的APP的列表

然后很多产品经理会在提需求的时候有一种洁癖,希望网页上那个可供用户点击的按钮,在已安装APP的时候是打开,在未安装的时候是下载

早先的做法

为了应对这样的需求,前端程序员们想出了一个很棒的方法,利用浏览器进驻后台之后会暂停js的执行这一特性,顺利实现了对是否安装了自己公司APP的探测。具体实现如下:

在用户点击或者进入页面时候,记录一个当前时间time1,然后设置一个300ms的timeout,在timeout的回调里面记录时间time2,接着就用location.href直接指向对应APP已经配置的url scheme。

如果time1time2的时间差超过400ms(考虑到setTimeout本身的误差)就认为安装了APP,不需要处理,否则则是没安装,提示下载或者自动下载。

因为,如果用户安装了对应的APP,那么会自动的跳转到APP的界面,浏览器这边的计时会被中断,等下次用户偶然再使用浏览器的时候,计时继续,会触发setTimeout的回调,不过这个时候time1time2的时间差就远大于300ms了。

如果用户没有安装对应APP就不会发生跳转,setTimeout也就自然在规定时间内运行回调了。

这种方法曾经是非常有效果的,不过现在会有些问题了。

首先是安卓下,有些浏览器,比如UC会在网页试图打开APP的时候阻断这个行为并弹出一个select弹窗,让用户选择是否打开,是否始终打开等等。

UC的select弹窗

但是这个弹窗并没有像alert那样阻塞js的执行,也就是说setTimeout会正常进行,在用户犹豫怎么选择的时候,300ms就过了,页面就自动跳转了下载页。

接着是IOS这边,到IOS10之后,如果已经安装了对应的APP,浏览器也会在网页尝试打开APP的时候弹出一个confirm窗,让用户选择是否打开对应的APP。

已安装:确认窗提示是否打开APP

如果没有安装对应APP,就会实现一个很丑陋的网页无法打开此链家的错误alert窗。

未安装:提示网址无效

而且这两个窗口也是没有阻断js脚本的执行的,setTimeout也是同样正常进行中。

尝试过把300ms的时间改成3000ms,去预想用户可以在3秒内在浏览器的选择窗内做好选择。可是仔细一想,这种预想又是非常不靠谱的,永远没法猜测用户的未知行为,而且3000ms的响应延迟也是非常不好的交互体验。

是时候去调整思路了

如果浏览器不希望我们知道设备上是否已经安装了对应的APP,那就不尝试去知晓。

目前我们的做法是:在用户通过点击或者别的行为触发了打开APP的策略的时候,首先是尝试直接访问url scheme,在这同时,展现一个弹窗,里面包含一个APP的介绍文案,然后底部是两个按钮,下载APP打开APP

下面是天猫的做法,很值得借鉴:

先尝试跳转
同时显示下载和打开

这样做的好处是:如果用户已经安装了APP,那么就直接跳转了,弹窗对用户不会有任何的影响;如果没有安装,或者用户安装了,但是因为不想跳转或者误操作没有跳转,则用户在关闭了浏览器的错误提示之后,看到弹窗,能大概理解之前的浏览器的行为是什么意思,同时可以再次选择打开或者下载APP。

同时,如果是进入页面立马就跳转的行为,最好能有一个次数限制,不然一个高频访问的页面,每进入一次都有这样一个弹窗,明摆着是想赶用户走了啊