Android Studio Projector远程开发的配置

Projector基本介绍

用JetBrains或者Android Studio开发项目有时候会有一个需求,就是本地的笔记本性能比较弱,或是有便携需求,或是系统不兼容(比如本地习惯用Mac,但是开发必须用Linux编译),或者单纯就是类似WSL的场景但是Linux里没有好用的图形界面,这个时候就希望能在远程机器上编译代码,而在本地机器上开发调试。

这个需求如果是VSCode,只需要装WSL、Remote等插件就能轻松搞定。VSCode的原理是在远程设备上运行了一个VSCode Server,这个Server负责做代码解析等操作,然后通过ssh把解析好的内容传输到本地,对带宽占用是很小的,稳定性非常好。

而同样的需求到了JetBrains上情况不太一样。在JetBrains Gateway推出之前,JetBrains的原理不太一样,整个IDE要跑在远程设备上,本机要想访问,要么直接走VNC一类方式直接把图像全部传过来,但是占用带宽大,要么用Projector,把IDE要显示的GUI元素信息传递到本机,然后在网页里面把GUI绘制出来。

Gateway是付费软件,而且好像不支持Android Studio,所以已经停止维护的Projector也还能凑合用(但是不支持最新版本Android Studio,我自己还在用的Android Studio 2021.2.1 Patch 1是没问题的)。这篇文章就是简单介绍Projector配置的一些问题。

Projector安装

Projector的配置基本可以参考这篇文章,但是还有一些坑需要填。
GitHub - joaquim-verges/ProjectorAndroidStudio: Guide to setup JetBrains Projector and access Android Studio from any device

Ubuntu中的安装

1
2
3
$ sudo apt update
$ sudo apt install -y python3 python3-pip libxext6 libxrender1 libxtst6 libfreetype6 libxi6
$ pip3 install projector-installer

下载好Android Studio Linux包,例如放在 ~/apps/android-studio 目录。运行 projector config add 输入AS的路径,就会马上生成一个Config。但是默认的Config是有一些问题的,需要做一下修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 查看config
projector config list
Checking for updates ... done.
1. Android_Studio

# 编辑配置
projector config edit Android_Studio
Checking for updates ... done.
Edit configuration Android_Studio
Enter the path to IDE (<enter> for /home/jzj/apps/android-studio/, <tab> for complete):
Use separate configuration directory for this config? [y/N]
Enter a Projector listening port (press ENTER for default) [9999]:
# 注意这里需要自定义listen的地址,建议直接填0.0.0.0,监听所有地址来的请求
Would you like to specify listening address (or host) for Projector? [y/N]y
Enter a Projector listening address (press ENTER for default) [0.0.0.0]: 0.0.0.0
Would you like to specify hostname for Projector access? [y/N]N
# 这里要选择使用secure connection,也就是https,如果用http,每次复制粘贴浏览器都会弹窗,只能通过弹窗复制里面的内容,巨麻烦
Use secure connection (this option requires installing a projector's certificate to browser)? [y/N]y
Would you like to set password for connection? [y/N]N
1. tested
2. not_tested
Choose update channel or 0 to keep current(unknown): [0-2]: 1
done.

运行下面的命令,在远程启动projector

1
projector run Android_Studio

然后在本机浏览器输入网址 https://<REMOTE_IP>:9999 即可访问,例如 https://192.168.5.110:9999/。 当然前提是本机可以访问这个地址,我用的是家里的局域网,也没有防火墙,所以直接就能访问。

HTTPS和PWA

但是有个很烦的问题来了,每次都会提示https的签名不对,每次都要在浏览器里点continue确认继续访问。

而如果你想长期使用Projector,每次在浏览器里打开,和众多标签混在一起也很烦。于是你想借助Chrome / Edge的创建App的功能,给这个网址生成一个单独的App(我们称之为PWA),有自己独立的窗口和图标,这样就会方便很多。但是,当https证书有问题的时候,PWA就会自动跑到浏览器里打开网页,这让我很不爽。一开始觉得是不是有Bug,好在我就是做Edge浏览器的,在同事群里试着问了一下,结果真就有人指出来了具体代码。看了下发现这个是Chromium有意设计的,不是Bug而是By Design,这里的考虑是PWA比一般的网页有更大的权限,如果有SSL错误就不安全,所以直接转到浏览器打开了。这里把代码也贴出来,注释写的挺明白的。

ssl_error_controller_client.cc - Chromium Code Search

安装自定义证书

由于以上两个原因,就必须想办法解决projector https证书的问题。网上找了半天,相关的资料太少了,结果最后发现挺简单的,projector本身的命令行就能搞定,运行下面命令生成一个证书(Certificate)给Android Studio。

1
2
3
Y9000P ➜  ~ projector install-certificate
Checking for updates ... done.
Installing autogenerated certificate to config Android_Studio

然后在远程可以看到Certificate在这里:

1
2
3
Y9000P ➜  ~ cd ~/.projector/ssl
Y9000P ➜ ssl ls
ca.crt ca.ini ca.jks

直接在本机运行scp把Certificate复制到本机:

1
2
➜  ~ scp [email protected]:/home/jzj/.projector/ssl/ca.crt ./                                                  
ca.crt 100% 2063 163.5KB/s 00:00

Mac上直接双击,默认会用Keychain安装到系统。然后打开Keychain Access,找到这个证书,右击菜单选Get Info,然后在Trust里选择Always Trust就可以了。重新启动Projector,重新启动浏览器访问,就发现证书已经没问题了。

最终的效果就是我可以安装一个PWA在里面跑Projector。

如果嫌每次手工启动Projector需要同时操作Server和本机太麻烦,还可以写个function搞定,保存到 .bashrc ,以后直接运行 startProjector 即可启动或重启Projector。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

stopProjectorServer() {
local HOST=$1
echo "Kill projector on host $HOST"
(ssh "jzj@$HOST" 'pkill -f projector')
}

startProjectorServer() {
local HOST=$1
echo "Start projector on host $HOST"
(ssh "jzj@$HOST" '/home/jzj/.local/bin/projector run Android_Studio' >/dev/null 2>1 &)
}

restartProjectorServer() {
local HOST=$1
stopProjectorServer "$HOST"
sleep 1
startProjectorServer "$HOST"
}

startProjectorClient() {
local HOST=$1
echo "Start projector client"
local APP_PATH="$HOME/Applications/Edge Apps.localized/Projector Web Client.app"
if [ -d "$APP_PATH" ]; then
open "$APP_PATH"
return
fi

# Projector must be ruuning with https, otherwise copy paste will be blocked by browser and unconvenient
open "https://$HOST:9999"
}

startProjector() {
local HOST="192.168.5.110"
restartProjectorServer "$HOST"
sleep 2
startProjectorClient "$HOST"
}

自动切换host

在这之后我又有了一个新的需求,我有多个运行Projector的远程主机,例如在家和在公司用的主机IP是不一样的,但是配置都是一样的,现在想随时切换不同的主机。

最简单直接的办法,是在shell脚本里传不同的HOST参数。但是这样会有个问题,PWA里的URL是固定的,我需要给每个HOST都装一个对应的PWA,不够方便。

能不能在本机 /etc/hosts 直接定义一个host来映射到不同的主机?这样我所有的操作都可以用这个host来操作,包括ssh,projector,adb等等。

这么做是可行的,下面的脚本可以用ping自动检测多个主机,按优先级能连上哪个就用哪个,然后设置到host中,之后就可以用 https://develop:9999 访问Projector了。

备注:部分代码是ChatGPT写的,Mac已经测试通过,没有在Linux上测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
setMachineAuto() {
local IP=$(getSuggestedMachineIp)
echo "Use '$IP' as develop machine"
setHostToIp 'develop' $IP
}

getSuggestedMachineIp() {
for IP in "192.168.5.100" "10.0.0.123"; do
if checkPing $IP; then
echo $IP
return
fi
done
}

checkPing() {
local target=$1
if ping -c 1 -W 1 "$target" > /dev/null 2>&1; then
return 0
else
return 1
fi
}

refreshDNSCache() {
echo "Refresh DNS Cache..."
if [[ "$OSTYPE" == "darwin"* ]]; then
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
else
sudo systemctl restart nscd 2>/dev/null || echo "No nscd service. Need to refresh DNS manually."
fi
}

# set host to ip mapping in /etc/hosts
setHostToIp() {
local host="$1"
local ip="$2"

if [[ -z "$ip" || -z "$host" ]]; then
echo "Usage: setHostToIp <HOST> <IP>"
return 1
fi

if grep -q -E "^$ip\s+$host(\s|$)" /etc/hosts; then
echo "Mapping already exists: ($(grep "$host" /etc/hosts))"
return 0
fi

echo "Old mapping: ($(grep "$host" /etc/hosts))"

sudo cp /etc/hosts /etc/hosts.bak

# check if host exists
if grep -q -E "^[^#]*\s+$host(\s|$)" /etc/hosts; then
echo " Set mapping: '$host -> $ip'"
if [[ "$OSTYPE" == "darwin"* ]]; then
sudo sed -i '' "/$host/ s/.*/$ip\t$host/g" /etc/hosts
else
sudo sed -i "s/^[^#]*\s\+$host.*/$ip $host/" /etc/hosts
fi
grep "$host" /etc/hosts
else
echo " Add mapping: '$host -> $ip'"
echo "$ip $host" | sudo tee -a /etc/hosts > /dev/null
grep "$host" /etc/hosts
fi

echo "New mapping: ($(grep "$host" /etc/hosts))"

echo "Remove ssh known_hosts for '$host'..."
ssh-keygen -R $host

refreshDNSCache

echo "Mapping done: '$host -> $ip'"
}

生成和安装Self-Signed Certificate

这么做又会遇到新的问题,浏览器再次报Certificate错误。

借助ChatGPT查了一番发现了问题所在,原来Certificate里是指定了域名的,而develop这个域名不在Projector自动生成的cert里,所以浏览器报错了。Cert里的这个东西叫做SAN(Subject Alternative Name)。

Projector命令行自动生成Certificate还是不太可控的,于是尝试自己直接生成Certificate。这种Certificate也叫Self-Signed Certificate,因为是自己生成的证书,而不是第三方颁发的证书。

先定义一个openssl.cnf 配置文件。注意:在最后面的alt_names里,可以添加所有你想用来访问的host和IP,其中IP不支持通配符,DNS支持通配符但是不建议直接写个 *,可能会导致浏览器判断出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
C = XX
ST = StateName
L = CityName
O = CompanyName
OU = CompanySectionName
CN = develop

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = develop
DNS.2 = projector
DNS.3 = localhost
IP.1 = 192.168.8.100
IP.3 = 192.168.5.211
IP.4 = 127.0.0.1

然后运行脚本生成Certificate,要用到前面的配置文件。其实配置文件里的参数也可以直接命令行传,但是写配置文件会更方便一点。

1
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -config openssl.cnf -extensions v3_req

注意:一定要在后面指定参数-extensions v3_req,如果不指定,这个alt_names就不会生成到Certificate里,而且你在生成的时候看不到任何报错,最后在浏览器里会报下面的错误。这个问题害我找了挺久才发现,ChatGPT有时候挺坑的……

1
2
3
Subject Alternative Name Missing The certificate for this site does not contain a Subject Alternative Name extension containing a domain name or IP address.

Certificate Error There are issues with the site's certificate chain (net::ERR_CERT_COMMON_NAME_INVALID).

运行完就会生成证书cert.pem 和 私钥key.pem,运行下面的命令可以查看cert的信息:

1
openssl x509 -in cert.pem -noout -text

如果cert没问题,应该可以看到SAN如下:

1
2
3
4
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:develop, DNS:projector, DNS:localhost, IP Address:192.168.8.100, IP Address:192.168.5.211, IP Address:127.0.0.1

实际在浏览器中,也可以看到一个网站的证书详情,点击URL左边的锁,点Connection is secure,再点里面的证书图标就可以了,知道了这个可以更方便的排查问题。比如Google的证书:

运行下面的命令把生成的Certificate安装到Projector中:

1
projector install-certificate --certificate cert.pem --key key.pem

然后把 cert.pem 下载到本机,我用的还是Mac,和前面一样用KeyChain App添加到系统,设置成信任就可以了。在系统KeyChain里,同样可以看到一个Cert的详细信息,包括前面我们生成时设置的SAN。

最后的效果就是,每次我的网络环境发生变化,就运行一下setMachineAuto,会自动根据我的网络找到最合适的机器,设置到系统host里。然后我所有的操作都直接用develop这个host访问就可以了,https不会报Certificate错误了,PWA也是没有问题的。