nswebfrog 2019-08-07T04:40:28+00:00 ccf.developer@gmail.com 我的 Windows 开发环境配置 2019-01-21T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2019/01/21/develop-with-windows 没错,这篇博客是来介绍我的 Windows 开发环境配置的。作为一个多年的 macOS 用户,最近对 Apple Macbook Pro 的产品质量以及 macOS 系统本身越来越多的 bug 感觉到厌烦,并且看不到有变好的迹象。加上转行做了后台开发,不再强依赖 macOS 系统了,最近又购买了一个 Surface Go 设备,能有一个非常便携的开发环境设备,这点还是非常吸引我的。便有了这个想法,就是将 Windows 打造成一个适合自己开发习惯的环境。

我本人的开发习惯重度依赖命令行,以前用过 Windows 下的 cmd 命令行,感觉并不是很好。但现在 Windows 10 有了 powershell,而且还有对 Linux 开发者非常友好的 WSL(Windows Subsystem for Linux)。在进行了一系列尝试后,有了一个目前相对满意的开发环境的配置。

常用软件

首先列出我目前在 Windows 开发的软件列表

  • 命令行: Cmder
  • Shell: zsh + oh my zsh
  • IDE: IntelliJ, VS Code
  • 编辑器: Sublime text 3
  • Docker for Windows

在微软商店中,下载 Ubuntu 18.04 作为日常使用的 WSL 系统。

Cap lock 按键映射为 Ctrl

作为重度 Vim 使用者,这是我最重要的习惯。因为主力键盘是 HHKB,这个习惯也是这么养成的。在 macOS 下完成这个配置很简单,只需要在设置中修改一下键盘的映射关系就可以。而 Windows 下需要修改注册表才能完成。修改方式如下:

注册表键值路径: Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout,如果没有,就新建一个二进制的键值,内容如下:

00 00 00 00 00 00 00 00
02 00 00 00 1D 00 3A 00
00 00 00 00

修改完毕后,重新登录 Windows 即可生效。

WSL 使用 docker

由于不习惯使用 Powershell, 所以我还是希望能够在 Linux 环境下使用 Docker。但是 WSL 并不是一个运行 Linux 内核的真正系统,Docker 的 Linux daemon 是无法正常工作的。但是有别的办法可以达到在 WSL 中使用 docker 的目的,那就是安装 Docker for Windows,然后在 WSL 中通过 TCP 来操作 Windows 环境中的 Docker。

首先,将 Windows 环境的 docker daemon 通过 TCP 端口暴露出来,打开 Docker for Windows 的 Settings 页面,在 General 选项中勾选 ‘Expose daemon on tcp://localhost:2375 without TLS’

在 WSL 的系统中,正常安装 Docker for linux, 需要注意,在安装完成后,不要启动 Daemon 服务(也无法正常启动),仅仅使用 docker client。想要在 WSL 里使用 Windows 的 Docker, 需要在设置好 DOCKER_HOST,将下面的命令加入到 .bashrc

export DOCKER_HOST=tcp://127.0.0.1:2375

然后就可以愉快的跟 Docker 玩耍了。

总结

虽说目前开发体验上 macOS 仍然是最好的,但有了 zsh + Docker,已经可以愉快的在 Windows 下进行开发了。而且最近无论是我自己的感觉,还是跟朋友聊天后总结,微软已经摆脱了之前那个视开源软件为毒瘤的形象,正在变得越来越 cool,相反 Apple 却没什么起色,甚至不如以前。从收购 Github, 到宣布 Github 私有仓库免费,微软正在一步步的转变。Surface Go 是我现在最喜欢的便携电脑,VS Code 已经变成我目前最喜欢的 IDE 了,而我 2016 款的高配 Macbook Pro 15 依然躺在家里用一个支架支起来,当成主机在用,主要是担心那个脆弱的键盘再出什么问题。

]]>
Nginx proxy_pass DNS 缓存设置 2018-09-09T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2018/09/09/nginx-proxypass-dns 问题描述

最近碰到了一个 Nginx 做为反向代理设置上的坑。起因是将 Nginx 做为反向代理服务器,来统一处理内网服务的转发。使用了类似如下的配置:

server {
    listen 80;
    server_name xxx.xxx.net;

    location / {
        proxy_pass http://xxxxx;
    }
}

刚开始的时候, proxy_pass 里使用的 ip 地址,Nginx 工作正常。近期由于内网服务升级,每个内网服务前面,都新增了一个 AWS internal load balancer,用来作为负载。而 AWS 的 lb 提供的访问方式,是一个内网 DNS 地址,而不直接提供 ip 地址。于是最初我便把 nginx 的 proxy_pass 里的 ip 地址改为了 AWS 提供的负载均衡的内网域名,测试后没有问题。但是在第二天一早到公司后,发现昨天配置的内网服务无法连通了。尝试执行命令 nginx -s reload 后,服务又恢复正常后,便没有过多追究,去忙别的事情了。但是一直感觉到这里的问题应该不简单,在详细查看 log 与文档后,发现了 Nginx 一个设置上的细节。

原因

在这个文档 Nginx 文档 Using DNS for Service Discovery with NGINX and NGINX Plus 中,解释了发生这个问题的原因。如果在 Nginx 的设置 proxy_pass 里使用域名而不是 IP 地址,Nginx 只会在每次启动和重载设置时,使用 DNS 将域名解析为 IP 地址缓存下来,并在之后一直使用这个 IP,并不会按照 DNS 的 TTL 刷新 IP 地址。如果在这个解析过程中发生错误,则会导致 Nginx 启动失败。由于在 AWS 中负载均衡的内网域名对应的 IP 并不是一直不变的,这才导致了上面的问题。文档中同样指出,使用 Nginx 的 upstream 配置也会有这个问题。

处理办法

既然知道了问题的原因所在,那么针对这个问题,根据上面文档中给出了一个解决方法,将配置文件修改为如下的形式:

server {
    listen 80;
    server_name xxx.xxx.net;

    resolver 172.31.0.2 valid=30s;
    set $service_lb xxxxxx;

    location / {
        proxy_pass http://$service_lb;
    }
}

在这个配置中,resolver 是 DNS 服务器地址, valid 设定 DNS 刷新频率。需要特别注意的一点是 set 语句不能写到 location 里面,否则不会生效。

参考资料

]]>
Linux 5G USB Wifi 选择及使用 2018-07-05T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2018/07/05/linux-5g-wifi 之前搭建了一个自己屋内的局域网环境,整个内部局域网的出口,是通过一台 miniPC 的 Wifi 连接到客厅的无线路由。去年自如将宽带升级为百兆光纤,而我的这台 miniPC 是占美 N3150 的机器,之前一直用自带的 300M Wifi 来连接,这个自带的 Wifi 显然就是我目前屋内网速的瓶颈,于是想要对其升级。一番分析之后,我需要一个这样的 Wifi 硬件: USB 3.0 接口,支持 5G 信号,并且可以在 Linux 下使用。经过一番比较,最终选择了一个推友推荐的硬件: EDUP EP-AC1622。这个设备的芯片使用的是 rtl8814au,官方提供了 Linux 驱动,但是支持的 Linux 内核版本比较低,无法在新内核版本下编译通过,但是好在有人提供了修改过的支持新内核版本的驱动,亲测可用。

环境

电脑安装的 Linux 发行版本是 Linux Mint 19, 对应 Ubuntu 18.0.4 ,内核版本 4.15.0-24.26

驱动安装

首先安装编译依赖工具:

sudo apt install linux-headers-$(uname -r) build-essential dkms git

依次执行以下命令:

git clone https://github.com/webfrogs/rtl8814AU.git
cd rtl8814AU
sudo cp -R . /usr/src/rtl8814au-4.3.21
sudo dkms build -m rtl8814au -v 4.3.21
sudo dkms install -m rtl8814au -v 4.3.21

所有命令执行成功之后,重启电脑,然后就可以使用 5G 的 Wifi 了。

最后

最近的使用情况来看,这个设备还是挺不错的,除了有些发热外,速度和信号确实要比之前好太多。实测内网的最快下载速度可以达到 20M/s,效果明显。

参考资料

]]>
macOS 逆向之生成动态注入 App 2018-02-09T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2018/02/09/make-injection-app-for-mac 前言

在 macOS 的逆向中,除了直接修改二进制文件外,针对使用 Objective-C 语言的原生 App,编写动态链接库来实现将代码逻辑动态注入到 App,也是一种逆向方式。将动态链接库注入到 App,一般通过两种方式,一种是修改 Mach-O 文件的 load command,而另外一种就是在运行 App 的二进制文件时,添加 DYLD_INSERT_LIBRARIES 环境变量,让 dyld 来加载我们的动态库。

利用 DYLD_INSERT_LIBRARIES 这个特性,我们可以自己构造一个全新的 App 来完成代码注入。当运行这个注入 App 之后,就会把我们的动态库,注入到指定的 App 中。目前常见的 macOS 的逆向的项目中,大多是提供一个 shell 脚本,这个脚本通过 ‘insert_dylib’ 或者类似的工具,修改原 App 的可执行文件来实现代码注入的。而通过构造动态注入 App 的方式,可以避免对原 App 的修改,对原 App 没有任何影响。如果用户通过注入 App 来打开,则可以运行我们修改后的代码,而如果打开的是原 App, 则是 App 本身的代码,而且只要原 App 升级后没有影响到我们 hook 的函数,用户可以正常对 App 进行升级。

App 文件结构

macOS 系统(包括 iOS 系统)的 App 文件,其实是一个文件夹,里面按照指定的格式,放置了 App 所需要的所有文件,比如可执行文件、图片资源、动态库等等。打开 Finder,在 Applications 文件夹中随便找一个后缀是 .app 的文件点击鼠标右键,选择 Show Package Contents,就能看到其中的内容了。在这些文件中, Contents/MacOS 文件夹里,放置的就是 App 的可执行文件, Contents/Frameworks 中,放置了 App 自带的一些动态库,而我们就从这两个地方着手,构建我们的注入 App。

生成注入 App

对一个 App 来说,可执行文件不一定非要是使用源代码编译链接后生成的,一个拥有执行权限的 shell 脚本,也是可以的。接下来,以注入 QQ 这个 App 为例来说明如何生成一个注入 App。

动态链接库

新建一个 macOS 下的动态库工程,最终生成名为 libQQInject.dylib 的动态链接库。为了演示,这个库的作用仅仅是在 App 打开后,在控制台输出一个 log,证明动态库已经运行起来。代码如下:

__attribute__((constructor)) void myentry() {
    NSLog(@"QQ is Injected successfully!!!");
}

创建 App

新建一个文件夹,命名为 QQInject.app,并创建一个子文件夹,名字叫 Contents

拷贝动态库

QQInject.app/Contents 下创建一个名为 Frameworks 的文件夹,将动态链接库 libQQInject.dylib 拷贝到这个文件夹中。

编写启动脚本

QQInject.app/Contents 下新建文件夹 MacOS,然后在这个文件夹下新建一个 shell 脚本文件,名字为 QQInject

注意:这个步骤中,脚本文件的名字必须与 App 名字保持一致。

以下是 shell 脚本内容:

#!/bin/sh
CurrentAppPath=$(cd $(dirname $0) && cd .. && pwd)
DYLD_INSERT_LIBRARIES=${CurrentAppPath}/Frameworks/libQQInject.dylib /Applications/QQ.app/Contents/MacOS/QQ

注意: 这里默认了 QQ 的 App 安装在了 /Applications 路径下

使用以下命令为脚本增加可执行权限

chmod +x QQInject.app/Contents/MacOS/QQInject

优化

至此,注入 App 已经可以运行了,试着双击以下这个新生成的 App,然后你会发现 QQ 已经运行起来,同时在系统的 console 中,可以找到一条日志:

QQ is Injected successfully!!!

注入成功,但是同时你会发现一些问题。接下来继续做优化

系统 Dock 图标

首先发现的一个问题就是,App 运行后,系统 Dock 图标不是 QQ 的图标,而是一个默认的应用图标。解决这个问题办法就是在 QQInject.app/Contents 下新建一个 Info.plist 文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>LSUIElement</key>
	<true/>
</dict>
</plist>

再次运行 QQInject.app 后,系统 Dock 上已经是 QQ 的图标了。

脚本化

上面的整个过程都是固定的流程,可以编写一个 shell 脚本来完成从动态库 build 到 注入 App 生成的整个过程。

最后

本文只是提供一个代码注入更加方便使用的思路,为了把逆向的结果更方便的使用。其中很多地方的逻辑并不是很严谨,比如被注入 App 不一定就安装在 /Applications 路径下,这就交给大家来自己写一个逻辑来判断啦~~

参考资料

]]>
iOS 逆向之网易云音乐去除开屏广告 2018-01-28T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2018/01/28/reverse-neteasemusic 最近更换了自己的 Apple 账号,然后发现网易云音乐在该区没有上架。由于不想在一台手机上搞两个账号互相切换,于是便想到将 App 重签名一下安装到手机中。同时,自己蛮讨厌开屏广告的,就顺手写了个 Tweak 来屏蔽开屏广告。期间有一些小坑,写篇博客来记录一下。

clutch 砸壳失败

第一步就是砸壳了,结果发现使用 clutch 砸壳出错,没有成功生成脱壳的 IPA 文件。具体原因没有深究,但是看错误提示,猜测应该是 clutch 不支持 StickerPack 的扩展导致的。

尝试使用 dumpdecrypted 来砸壳,测试成功。砸壳的方法参考了这篇博客。得到砸壳后的可执行文件之后,从越狱机中将 App 拷贝出来,替换掉可执行文件即可。

Theos 默认不支持 ARC

这个是最近发现的,原来 Theos 默认并不使用 ARC 编译 Objective-C 文件。解决办法是在 Theos 工程的 Makefile 中添加下面一句:

ADDITIONAL_OBJCFLAGS = -fobjc-arc

Tweak 实现原理

由于开屏广告的显示时间非常靠前,不可能等到 App 启动后,从网络获取到数据后再显示,一般的做法就是提前加载好之后要显示开屏广告数据缓存到本地,App 每次刚启动后,直接从本地取出数据,然后显示需要的开屏广告给用户。

将二进制拖到 Hopper 中,搜索关键字 advertisement 浏览一下搜索结果,很快便发现了一个名叫 NMAdvertisementManager 类。一番分析后,实现逻辑的 Tweak 的代码如下:

%hook NMAdvertisementManager

- (void)fetchStartupAdvertisement {
	[[%c(NMAdvertisementManager) defaultManager] performSelector:@selector(clearCachedResources)];
	[[%c(NMAdvertisementManager) defaultManager] performSelector:@selector(removeOldAdItems)];
}
%end

逻辑是:hook 了获取数据的函数,每次这个函数被调用的时候,就清除本地缓存的广告数据。这样本地没有了广告数据,也就不会再显示开屏广告了。

最后

虽然我理解 App 加开屏广告的初衷,但是作为一个用户,真的非常讨厌这个行为。有些 App 甚至是在程序从后台切到前台的情况都会显示广告,非常烦人。好了,用逆向来把生活变得美好一些吧。

参考资料

]]>
使用 Systemd 自动续期 Let's Encrypt 证书 2017-12-14T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2017/12/14/letsencrypt-renew 今早发现 Melisandre 官网的 Let’s Encrypt 证书过期了,可是明明记得自己之前是配置了自动续期的啊。经过一番排查发现,原来之前使用的 certbot 官网推荐的 autorenew 方式,证书是刷新了,但是由于 nginx 没有重启,所以新证书没有生效。那准备改为使用 Systemd 来自动给证书续期。

先来添加一个 service:

$ sudo vim /etc/systemd/system/letsencrypt.service
[Unit]
Description=Let's Encrypt renewal

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --agree-tos
ExecStartPost=/bin/systemctl reload nginx.service

然后添加一个 systemd timer 来定时触发这个服务:

$ sudo vim /etc/systemd/system/letsencrypt.timer
[Unit]
Description=Daily renewal of Let's Encrypt's certificates

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

启动服务,开启 timer:

$ sudo systemctl enable letsencrypt.timer
$ sudo systemctl start letsencrypt.timer

可以通过命令 systemctl list-timers 来查看 systemd 所有的定时服务。

]]>
Swift 实践篇之链式 UI 代码 2017-10-20T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2017/10/20/swift-practice-ui-chaining-code 前言

本篇博客主要介绍 Swift 实践方面的一个技巧,链式 UI 代码。链式代码在 Swift 中有着比 Objective-C 天然的优势。而且通过 Swift 语言本身强大的特性,只需要很少的代码就可以让自己的 Swift 工程具有编写链式 UI 代码的能力。

缘由


先来回答一个问题,为什么要用链式代码来写 UI?答案就是,提高代码可读性。代码可读性的重要性在软件工程的意义不必多说。而代码可读性可以从许多方面优化,比如,合适的缩进、正确的命名、清晰有逻辑的结构等等。链式 UI 代码正是从清晰有逻辑的结构这点出发,提高代码可读性的一种方案。

在 iOS 开发中,我们每天都要 UI 代码打交道。而在 UI 的实现方面,用 Storyboad 实现还是代码实现,是 iOS 开发领域中一个永不停息的争论。两者各有优势,关于这个的讨论也不在本篇博客范围内。不过无论是用何种形式写 UI, 代码或者 Storyboard, 如果整体的 UI 布局没有一个清晰的分层和结构,那么维护起来都会吃力。本篇的重点在于介绍一种代码风格,一个使用链式代码来写 UI 布局的代码风格,而不在于讲解如何将 UI 布局做得结构清晰。

前置条件


有一些前置条件需要交代一下。接下来的介绍中,UI 布局通过代码来完成,而且使用的是 AutoLayout 而非绝对布局。相信没有人会用原生的 Autolayout API 来写全部的 UI 吧,一般会选用一个封装库。博客下面的介绍以最常见的 Autolayout 封装库 SnapKit 来举例,如果你的工程使用了别的库,根据情况来修改相关的代码即可。

核心代码实现


先来分析一下 UI 布局代码的结构。翻出一段使用代码完成的 UI 布局代码看一看,你会发现其中的一些特定的结构。

第一步肯定是会初始化一个 view,接下来通过一系列步骤完成需求:添加到父 view、添加视图约束、配置属性。在这接下来的三个步骤里,添加到父视图这个步骤一定要在添加视图约束这个步骤之前。至于配置 view 属性这个步骤,在这两个步骤之前或之后都没有问题。

除了初始化 view 这个步骤,将其后的三个步骤抽取出来,做成三个通用的链式布局函数,将这三个函数使用 extension 机制扩展到 UIView 中。实现代码如下:

import UIKit
import SnapKit

protocol ViewChainable {}
extension ViewChainable where Self: UIView {
    @discardableResult
    func config(_ config: (Self) -> Void) -> Self {
        config(self)
        return self
    }
}
extension UIView: ViewChainable {
    func adhere(toSuperView: UIView) -> Self {
        toSuperView.addSubview(self)
        return self
    }
    @discardableResult
    func layout(snapKitMaker: (ConstraintMaker) -> Void) -> Self {
        self.snp.makeConstraints { (make) in
            snapKitMaker(make)
        }
        return self
    }
}

以上就是完成链式 UI 布局的所有代码。仅仅 20 多行代码就完成了整个核心功能,是不是很棒?不需要引用任何第三方库,只要将这段代码拷贝到 Swift 工程中,你的工程马上就有了写链式 UI 代码的能力。

来个样例说明用法,比如,我们要在一个 view 中添加一个 label,这个 label 水平居中,它的 top 与父 view 的 top 相距 80。代码如下:

let label = UILabel()
    .adhere(toSuperView: view)
    .layout { (make) in
        make.top.equalToSuperview().offset(80)
        make.centerX.equalToSuperview()
    }
    .config { (label) in
        label.backgroundColor = UIColor.clear
        label.font = UIFont.systemFont(ofSize: 20)
        label.textColor = UIColor.darkGray
        label.text = "Label"
    }

那么这段样例代码有什么好处呢?首先,从这段代码中,可以明显看到 UI 布局的步骤,而且添加约束和配置 view 的具体逻辑代码,被不同的 Closure 封装起来。代码的结构要比之前的写法,代码都用同一个缩进的代码结构要更清晰一些。当定位到 UI 的问题后,可以根据问题是布局错误还是配置问题,直接去分析对应的代码逻辑。

这样的代码还有一些别的好处,那就是 UI 配置相关代码的复用性提高。来,针对上面的代码的 config 部分做如下的修改:

let labelConfiger = { (label: UILabel) in
    label.backgroundColor = UIColor.clear
    label.font = UIFont.systemFont(ofSize: 20)
    label.textColor = UIColor.darkGray
    label.text = "Label"
}
label.config(labelConfiger)

完成与之前同样功能的代码,但是这里定义了一个类型为 (UILabel) -> Void 的 Closure,并将其作为参数直接传递给了链式函数 config。想想,如果接下来新建一个名为 label2 的 UILabel,配置跟 label 一样,那个它的 config 函数直接接收这个 labelConfiger 的 Closure 作为参数就可以完成配置。你可以能会问,如果这两个 label 的文本值不一样的时候怎么办呢。这种情况,只需要将代码改动如下:

label.config(labelConfiger)
    .config { (label) in
        label.text = "Label1"
    }

label2.config(labelConfiger)
    .config { (label) in
        label.text = "Label2"
    }

没错。作为链式代码,config 函数可以多次对自身调用。不过需要注意的是,针对视图的同一个属性,后面调用的 config 函数会覆盖掉前面的 config 函数里设置的状态。

在 Swift 中,Closure 是可以被当做变量的,而且就连函数其实也是一种 Closure。所以,接下来就可以自由发挥啦~~

分析实现


来讲解一下链式核心代码的实现。 链式函数的关键就在于返回值类型与自身相同。

由于是 UI 相关,那么首先是对 UIView 这个视图父类做扩展。adhere 这个函数没啥可说的,就是讲自身添加到通过参数传入的父视图上。接下来是 layout 这个布局函数实现,由于是使用 SnapKit,而且具体的布局逻辑不同的视图也不一样。所以将使用 SnapKit 布局时的关键逻辑代码以 Closure 闭包参数的形式传入。这里比较难理解的是 config 函数的实现,可以看到这里定义了一个名为 ViewChainable 的协议,然后对这个协议做了一个扩展,并让 UIView 实现了这个协议。这么做的原因在于,为了让 config 函数的闭包参数在实际使用中,闭包的第一个参数类型可以具体到 UIView 的不同子类上。拿上面的例子来说,UILabel 的实例在调用 config 函数时,所需的闭包参数类型就是 (UILabel) -> Void 而不是 (UIView) -> Void

优化


不知不觉文章已经很长了,不过还是希望你读下去。接下来是对上面的链式核心代码的优化。

由于上面的实现是直接针对 UIView 做了方法扩展,这在实际的工程中会碰到一个问题:命名冲突。举例来说,如果下一个版本的 SDK 更新后,UIKit 给 UIView 新增了一个 adhere 函数,参数和返回值类型和上面的定义完全一样,那如果发生这种情况,就只能修改自己之前的代码了。

Objective-C 时代的一个通行做法是对系统库的扩展方法加一个至少三个字符的前缀,这样函数名字就变成了这样:xxx_adhere 。当然你可以按照这种形式,给之前的链式 UI 函数名字都加上一个前缀,这么做是没有任何问题的。可是在 Swift 的世界里,有着更 Swifty 的实现形式,我在之前的博客文章 Swift 命名空间形式扩展的实现 中具体介绍这种形式的实现方式。那篇文章里提到的 HandOfTheKing 这个工程,里面已经包含了命名空间形式的链式 UI 代码实现。需要注意的是,在最近的一次更新里,我把这个项目的命名空间前缀由之前的 hk 改为了 hand,hk 用起来总感觉跟香港有关。哎,谁让命名是编程世界的一大难题呢~~下面贴出具体实现的代码(这里不包含命名空间的实现)

import UIKit
import SnapKit

extension UIView: NamespaceWrappable { }
extension NamespaceWrapper where T: UIView {
    public func adhere(toSuperView: UIView) -> T {
        toSuperView.addSubview(wrappedValue)
        return wrappedValue
    }

    @discardableResult
    public func layout(snapKitMaker: (ConstraintMaker) -> Void) -> T {
        wrappedValue.snp.makeConstraints { (make) in
            snapKitMaker(make)
        }
        return wrappedValue
    }

    @discardableResult
    public func config(_ config: (T) -> Void) -> T {
        config(wrappedValue)
        return wrappedValue
    }
}

改成这个之后,之前的 label 的代码就变成了下面的风格:

let label = UILabel()
    .hand.adhere(toSuperView: view)
    .hand.layout { (make) in
        make.top.equalToSuperview().offset(80)
        make.centerX.equalToSuperview()
    }
    .hand.config(labelConfiger)

进阶


如果你对响应式编程感兴趣,你会发现 RxSwift 这个框架和上面的链式布局代码简直是绝配。RxSwift 这个库本身也是支持很多链式调用的。关于 RxSwift,这又是另外一个大坑了,本篇就不多讲了。

结尾


昨天在 V2EX 上看到一个帖子问 2017 年的 iOS 开发是用 Objective-C 还是 Swift 的。对于习惯了 Swift 的我而言,已经不想再回头写 Objective-C 了。当然在最后,还是要继续吐槽一下 Xcode 这个不争气的 IDE。配上一声长叹,来结束本篇博客,哎……

]]>
一个 macOS 独立项目的诞生记 2017-09-18T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2017/09/18/the-born-of-melisandre-app Melisandre 是我刚刚发布的一款 macOS 的窗口管理软件,特点是使用快捷键来自由移动和缩放窗口(能达到跟鼠标拖动一样的效果)。她是第一个完全由我自己从零开始完成的作品,名字来源于冰与火之歌里侍奉光之王的红袍女 Melisandre。

整个项目的实现过程中,从 logo 和图标的设计到客户端,web 端,后端的开发,全部自行完成,也算是当了一把全栈程序员。欢迎访问 Melisandre 的官方网站来下载体验。

起始


灵感来自于几个月前的一天,在某次鼠标和键盘间不停切换来处理窗口移动和键盘输入的时候。突然想到如果有个软件能够使用键盘来控制窗口,那不就可以避免这种情况了么。于是便去找了一些现有的窗口管理软件来使用。然而发现我找到的窗口管理软件都是支持固定大小的窗口布局,比如屏幕的二分之一,三分之一。而我希望的是一个可以自由移动的窗口管理软件。“既然没找到,那就自己撸一个吧”这就是我当时的想法。

技术栈


Melisandre 1.0 版本完成时用到的技术栈及环境:

  • 客户端:Swift 4, Xcode 9 beta
  • 官网 web 页面:js + Vue
  • 后台 API 接口:Golang
  • 数据库:PostgreSQL
  • icon 与图标设计: Sketch

服务器用的是 Vultr 的美国机房,官网页面支持 https 和 http2。

从零开始到发布上线


首先,便是调研实现客户端功能所需的技术。所有窗口管理软件使用的第一步都是需要给 App 授权 Accessibility 使用,那便去学习了 Accessibility API 的文档。虽说 macOS 开发和 iOS 开发有些不同,但花了一些时间后,还是很顺利实现了我自己的需求:使用快捷键自由移动和缩放窗口。

接下来,如何分发软件。最初的想法是上架 Mac App Store 的(这里就给自己挖了一个大坑,后面会说),毕竟 App Store 里有不少同类型的 App。最近软件自动订阅付费成为了一种趋势,于是便去学习了苹果的 In-App Purchase 的文档 以及 Raywenderlich 上的这个教程。一边着手接入 Auto-Renewable Subscriptions,一边完善 App 的其余功能及 UI。

于此同时,我把 App 分享给了一些同事来试用。感谢这些同事,并没有因为 Demo 版本的丑陋就拒绝使用,反而提出了不少有用的意见和建议。综合了这些意见之后,我发现目前的窗口管理软件所使用的固定布局确实是一种很方便的管理方式。那既然要把 Melisandre 作为正式发布的产品,这种模式还是必不可少的。思考一番之后, Melisandre 的第二个重要功能 Pushpin(图钉) 诞生了。Pushpin 功能总结起来就是:通过预设的窗口位置,像使用图钉一样,使用快捷键把窗口钉到屏幕的指定位置。而且 Pushpin 更加灵活,自定义的程度更高。

这期间还 Melisandre 还加入了另外一个实用的功能:多显示器下的窗口移动。这虽然也不是原创功能,但是在细节上,我花费了一番功夫去调整,使得窗口在多显示器间切换显得更加自然。Melisandre 控制的窗口切换显示器后,窗口在新显示器下的位置与之前的位置相同,但是窗口大小不会变化。如果两个显示器分辨率不同。那么之前的宽度或者高度的撑满的状态在下个显示器里会被保持。其实上面的逻辑不需要专门去理解,总之就是为了让窗口在多显示器的切换更加自然。

关于付费,目前版本的 Melisandre 基础功能是可以免费使用的,包括移动和缩放窗口,预设的四个 Pushpin 功能。但是一些自定义设置,比如快捷键的自定义和 Pushpin 的完整功能需要付费解锁。付费采用订阅制,而不是之前常见的版本买断,这也是我想去做的一个尝试。

在一切准备就绪的时候(包括 Apple 的订阅功能也已经开发完成),接下来就碰到了 1.0 版本发布一个大坑:无法上架 Mac App Store!!前面已经提到,整个 App 的核心,控制窗口移动的这个功能,是使用 Accessibility API 来实现的。苹果对上架 App 的要求是必须使用 Sandbox,而一旦对 App 启动了 Sandbox,Accessibility API 就无法控制其它 App 的窗口了(自己的窗口还是可以控制的)。这其实也是我的粗心,我是在未开启 Sandbox 的情况下完成了窗口的功能,之后开启了 Sandbox 开始 接入和测试订阅付费功能,期间一直没有去验证之前的窗口功能的可用性。

没办法,只能放弃 Mac App Store 上架这条路,而选择自己渠道发布。你可能会问那现有商店里的窗口管理软件是怎么上架的呢。这是由于他们上架都非常早,上面的那条限制只对新 App 有效,不过可以预见的是,苹果会逐步关闭这个通道。不过按照 macOS 现在这个爹不亲,妈不爱的状态,也难说。接下来就是自己发布所碰到了一连串的问题了。

首先是支付的问题。调研一番后,国内的服务就先排除了,已经没有针对个人的支付接口了。本来打算使用 stripe 来接入支付,但发现账户激活需要美国的身份,遂放弃。最终选择了 Fastspring 的支付服务,看了 Fastspring 的文档后,发现其功能做的相当完善。接入后,连 E-mail 服务都提供好了,当然代价就是收费贵。

其次是 License 的管理,由于我想采用订阅付费的授权形式,就没有找到合适的现成服务。这里提一下,如果使用买断式的 License,国外有个叫 DevMate 的服务可以使用,免费版本配合 Fastspring 已经足够用了。我最后是自己实现了一套 License 机制,使用 hmac hash + RSA 来验证。

由于是自己的实现,就需要一个后台服务来生成 Licese,这个后台服务本来打算用 Swift 来写的。但是 Swift 本身语言还不稳定,服务器开发环境也不成熟,考虑到后期的维护成本,就放弃了这个想法。在 Python 和 Go 中间犹豫了很久之后,最终选择了 Go 实现了这个后台。数据库系统选择了 PostgreSQL。这里不得不说一句,使用 VS Code 来开发 Go,体验真是好的不要不要的,甩出 Xcode 下写 Swift 一条街。

最后是客户端的收尾工作,由于之前为了上架 App Store,一些功能受限,而去掉 Sandbox 后,居然还有些功能不能正常工作了。于是对整个 App 测试整理,并调整了一些 UI。接着又用 Vue 撸了一个官网的页面。1.0 版本总算是可以发布了,于是在 iPhone X 发布的当天,Melisandre 1.0 版本上线了。

然后就是服务器的配置了。嗯,独立开发嘛,运维也要自己做。Go 语言的一个好处就是部署很简单,省了不少事情。官网的网页就是静态文件,一个 nginx 网关就可以直接提供服务了。

目前 Melisandre 最新的版本是 1.0.1。主要是接入了 Sparkle 的版本自动更新服务。这也是非 App Store 应用里必要的功能了。其实 1.0 版本已经接入了 framework,但是有一些配置的问题,1.0.1 中修复了一下。

这期间碰到的技术细节其实还有很多,之后另开博文来写吧。

上面的没有提到设计的过程,其实设计是一直穿插在开发中进行的。虽然作为一个后台服务的 App,涉及的页面并不多,但是还是有一些的图标需求的。作为一个设计门外汉,我是一边摸索一边学习,找来了一些 icon 的素材,结合自己的需要做一些改动。包括官网 web 页面的设计,也是借鉴了不少的网站照葫芦画瓢完成的。这里不得不感叹一下 Chrome 浏览器的 inspect 功能,真的非常好用。虽说设计得很业余,但最终的效果我还是很满意的。

总结


由于我选择了一条困难的路径,整个过程还是非常折腾的。不过总算是从零开始完整地实现了一个产品。全栈技能 ++,还有就是收获了满满的打破舒适区的成长感。

当然对用户来说,他们并不会关心你背后的技术怎么实现。他们只需要你的产品能够解决需求,同时用起来舒服,如果能再好看一些,那就更完美了。Melisandre 还有很多可以优化的空间,接下来的时间再一步步完善了。

独立开发不易,同时也深刻体会到 App Store 是项伟大的发明,它使得开发者将精力聚焦在好的 App 实现上,而不用太过关注发布和更新。对独立开发来说,把 App 上架绝对是一件省时省力的途径。

虽说是选择了一条困难的道路,但还是有不少现成的服务可以使用的,国外针对这样的服务还是有不少的。我目前选择的方案,都是我在调研或者看了别人分享的经验之后所选择的。不一定是最优,如果你知道更好的方案,欢迎一起来讨论。

我觉得以后应该还会有更多的独立作品。

不知不觉已经写了不少了,非常感谢你能耐心看完。如果觉得对你有帮助,不妨请我喝杯咖啡,又或者…买个 Melisandre 的 License 呢,嘻嘻。

]]>
用 Docker 搭建 Jekyll 博客的 CI 2017-08-23T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2017/08/23/build-jekyll-ci-with-docker 最近在抽空学习 Docker。知识嘛,在实际场景中来使用永远是最好的学习方法。于是想到了将本博客的部署系统 Docker 化。

现状


既然是要优化,那先来介绍一下博客系统的现状。博客文章都是 Markdown 格式的文本,使用 Jekyll 加载 HTML 模板,生成最终的博客网页。Jekyll 生成的网页是静态的,托管在我的 VPS 上,VPS 上有个 nginx 网关来提供 Http 服务(已支持 https 和 http2)。目前的模板是我基于一个开源的模板修改而来。整个博客内容使用 Git 进行版本管理,托管在 bitbucket 上的私有库里(在我的 Github 主页可以找到这个库的老版本代码)。

在这之前已经针对博客的部署流程做了相应的优化。目前达到的效果就是,每当新博文完成后,将对应的新的 Git commit push 到远端仓库,然后就完事儿啦!之后的事情都是自动化处理: 托管博客服务的 VPS 会更新博客仓库,调用 Jekyll 生成最新的静态网页,最后呈现最新的博客内容。

原理很简单,VPS 有一个 http 的 webhook,然后在 bitbucket 仓库里配置好这个 webhook,当有新的 commit 过来后 webhook 会被调用,VPS 上的 webhook 在收到请求后,就会自动执行接下来的流程。现在的方案里,这个 webhook 是用 Swift 来实现的,我之前一篇文章里专门介绍了实现。

Docker 化


Docker 跟功能的开发并没有太大的关系,其关注的方向是持续交付和部署,也就是希望达到一次创建和配置,可以在任意地方正常运行。就拿我的博客服务来说,目前 Swift 写的这个 webhook 是使用 Supervisor 来保证服务的正常运行。如果我需要更换到另外一台机器上部署同样的环境(最常见的情形就是换个 VPS 服务商),可能由于相隔时间久远,好多配置的写法都不记得了,那配置新服务器难免还是要折腾一番的。Docker 正是解决这种问题的一种非常好的方案。将相应的配置和运行环境做成 Docker 镜像,然后新服务器上只需要几个简单的 Docker 命令,就可以轻松的将服务运行起来。怎么样,是不是很诱人呢?

制作 Jekyll CI 镜像


分析一下需求,这个镜像的功能总结如下:

输入: http 形式的 webhook
输出: 最新的博客网页内容

最终的实现上,我弃用了 Swift 写的 web 服务,而只使用 ruby 语言来完成整个功能。原因很多,一是 Swift 本身语言并不稳定,服务器端的开发更是如此。而且 Swift 对应的 Docker 镜像体积巨大,而且官方只提供到了 3.1 版本(目前最新的 Swift 版本是 3.1.1)。二是 Jekyll 是用 ruby 写的,所以 ruby 的环境是少不了的,既然如此,不如整个环境都用 ruby 好了。正好趁机也学习 ruby 语言。

最终用于构建镜像的 Dockerfile 内容如下:

FROM ruby:2.4.1-alpine3.6 

RUN apk update --no-cache \
    && apk add --no-cache --virtual .build-deps \
        build-base \
    && apk add --no-cache git openssh \
    && gem install \
        jekyll \
        jekyll-paginate \
        sinatra \
        thin \
    && apk del -f .build-deps

WORKDIR /opt/blog/
COPY hook.rb /opt/blog/

VOLUME /root/.ssh
VOLUME /opt/blog/sites

CMD ["ruby", "hook.rb"]

基于 alpine 的 ruby 镜像构建出来的最终镜像大小感人,只有 140 M。这里的 hook.rb 就是提供 http 服务的 webhook。一切准备就绪后,使用 docker build 命令生成镜像,然后把镜像 push 到我的 docker hub 中。这样,我在 VPS 里就可以直接 pull 到镜像然后部署了。

在这个 webhook 的实现里,有一个让我折腾了不少时间的地方就是由于需要执行 Git 命令,而我的博客的是私有仓库,对应的有获取权限的私钥如果要打包到 Docker 镜像中,只能使用 Docker 的私有镜像库了。并不想这么做,于是最终的写法是将私钥通过 Docker 的 Volume 机制,将宿主机里的私钥共享到容器中,这样就可以了。

webhook 的具体实现上,用了 sinatra 框架做为 http 服务,在收到的请求里,执行对应的 shell 命令。而用 ruby 来执行 shell 命令比我预计的要简单许多。完整的代码我已经放到了我的 Github 库里: 戳这里。对应在这个库里目录名是 blog-ci。在这个仓库的根路径下还有一个名为 blog-init.sh 的脚本,里面封装了对应的启动 Docker 容器的命令,配置相应的 Volume,并将 http 服务绑定到本地的一个端口。当我需要在新的机器里部署的同样环境时候,只需要一键执行这个脚本就可以啦。

最后,由于我的 VPS 里是用 nginx 做为网关的。Docker 容器起来之后,http 服务只绑定了本地的端口,配置好 nginx 网关的对外中转后,重启 nginx 服务就生效了。

总结


Docker 的使用方面有很多,这里只是在我的实际需求中做了一次小的尝试。感觉 Docker 的很多使用方式跟 Git 很像,而且 Docker 的可定制化程度相当高。我在最近的使用中就有一个想法:以后电脑中就只安装一个 docker 环境就足够了,就不用再折腾别的许多环境了,比如 ruby 的版本配置之前就搞得我很是头疼。不过这么用的时候, Docker 的指令也会显得很繁琐。比如,如果你的本地没有安装 ruby 环境,但是想编译 Jekyll 的博客,就可以通过我做的一个 Jekyll build 镜像来完成:

docker run --rm -v $(pwd):/srv/jekyll nswebfrog/jekyll jekyll build

也可以用以下命令使用 Jekyll 自带的 server 命令:

docker run --rm -p 4000:4000 -v $(pwd):/srv/jekyll nswebfrog/jekyll jekyll serve --host=0.0.0.0

如果你使用了很多相关的插件,这个镜像或许就不能满足你的需求了。不用担心,对应的 Dockerfile 已经包含在了上面提到的 Github 仓库中,通过简单修改,你可以很轻松地获得一个适合自己需求的镜像。

好了,本篇博客也该结束了,我要 push 最新的博客了。来吧,Jekyll 自动化!!

]]>
第一个 macOS App 诞生 2017-08-14T00:00:00+00:00 nswebfrog https://blog.nswebfrog.com/2017/08/14/my-first-mac-app

花费了整个周末的时间完成了一个 macOS 的 App。功能总结起来就一句话:

使用键盘快捷键来移动窗口。

目前的版本,快捷键还不能自定义,使用的是 vim 的方向键加上 cmd 和 ctl 的组合键来移动当前被 focus 的 macOS 窗口。

按照我一贯使用冰与火之歌人名来给项目起名的惯例,我将它命名为 Melisandre。这也是从设计到开发全部是自己完成的第一个 App 了,虽然功能简单,目前也只完成了最基本的功能。但是也碰到了不少问题,主要是在 Sketch 以及 macOS 开发的熟悉上花费了不少功夫。

macOS 的开发确实要比 iOS 麻烦。在碰到问题时,iOS 上基本不用费太大功夫就能找到解决方法,而 macOS 则资料并不是很多。期间碰到的一些知识点我觉得还是蛮有意思的,稍后有时间会再写博客来记录一下。而现在 Mac App Store 采取的沙盒机制,对 App 的开发确实限制不小,比如我想要在 Melisandre 中实现的 App 开机自启动的功能,沙盒里的实现方式感觉很 tricky。 Melisandre 倒是符合了沙盒机制,不过目前功能过于简单,估计上架的话会被拒。

这个 App 作为工具类型的软件,UI 除了 logo, 目前也就一个偏好设置界面。里面涉及的一些图标都用 Sketch 来完成的。不过除了 App 的 logo 我是花了一些精力来设计,其余的 icon 基本都找了现成的资源来用的。这点上,感觉设计和开发还是蛮像的,很多东西都可以用现成的资源来搞定。

有时间接下来再继续完善~~

丢一个下载地址: 点我下载

]]>