Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·1336 lines (1252 loc) · 46.1 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·1336 lines (1252 loc) · 46.1 KB
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/sh
#
# Installs a Ghost node for the current user and starts it mining. It asks
# nothing: paste the one line from the README,
#
# curl -fsSL https://get.ghostcoin.com | sh
#
# and when it finishes the node is installed, on the network, syncing and
# mining, and it prints the mining address and how to check on the node.
#
# The script downloads the release archive for this computer and its
# signature, and verifies the signature against the release key pinned below
# before it unpacks or runs anything. The check is done by ghost-verify, a
# small static program (install/verify) downloaded from the same release and
# run only when its SHA-256 matches the one pinned below for this computer. On
# any doubt about a download, a hash, the signature or the archive it stops
# with nothing installed and says why.
#
# It then installs the node in ~/.ghost-node, writes the node's config with a
# newly generated mining address (or GHOST_MINE_TO's, below), and starts the node as a user service with
# mining on, which also starts it at boot: a systemd user unit on Linux, a
# launchd agent on macOS. The node joins the network built into it, the public
# test network; an archive that still holds a genesis.json gets that chain
# initialised first. The config sets no network and no peers, so the node
# finds the public test network through the seed nodes built into it, and on
# that network it mines only while it has a peer. The config opens the node's
# HTTP RPC on 127.0.0.1 only, for the Ghost web apps.
#
# It also installs ~/.ghost-node/update, which is this script run with
# --update, and runs it daily (a systemd user timer, or a launchd agent with a
# StartInterval) unless GHOST_NO_UPDATE=1 is set at install time. An update
# first downloads the install.sh of the newest release, from newest_url below,
# and its signature, install.sh.sig, and checks that signature against the
# release key with this script's own pinned ghost-verify. Only then does it
# run that install.sh with --update, which passes the same checks as an
# install with the newest release's own pins, replaces the geth executable
# atomically, keeps the previous one until the node has restarted, and keeps
# the config and the mining address. A refused update changes nothing and
# leaves the running node alone. Run again, the installer installs its release
# again and keeps the config and the mining address.
#
# This copy in the repository downloads from the latest release. The copy a
# release publishes is pinned to its own release: install/pin-verifier-hashes.sh
# writes in that release's ghost-verify hashes and replaces release_url's
# latest/download URL with the release's own download URL, so the script never
# fetches a ghost-verify it does not pin. newest_url stays latest/download:
# it is only where an update finds the newest release's signed install.sh.
#
# It needs only sh, curl, tar, gzip, uname, sha256sum or shasum and the usual
# POSIX tools that stock macOS and Linux have; no OpenSSL of any version.
#
# The release archive is ghost-node-<os>-<arch>.tar.gz, with os linux or darwin
# and arch amd64 or arm64. It holds the geth executable. The signature,
# <archive>.sig, is the raw 64-byte ed25519 signature of the archive's bytes.
# The checker is ghost-verify-<os>-<arch>, next to the archive.
#
# GHOST_RELEASE_URL sets where the release is downloaded from, over https
# only, and GHOST_INSTALL_CA_FILE names a certificate file to trust for that
# server; GHOST_LATEST_URL, over https only and trusted the same way, sets
# where an update looks for the newest release's install.sh; GHOST_HTTP_PORT
# moves the node's HTTP RPC off 8545. The tests use them for a local release
# server. When another program already listens on 127.0.0.1 at that port, the
# installer stops with nothing installed and says so; the installed node
# itself holding it, as on an update, is fine.
#
# GHOST_MINE_TO mines straight to a wallet address of your own, such as the
# Ghost pop-up wallet's, instead of a newly generated mining key:
#
# curl -fsSL https://get.ghostcoin.com | GHOST_MINE_TO=0x... sh
#
# (on Windows, in PowerShell: $env:GHOST_MINE_TO='0x...'; irm https://get.ghostcoin.com/win | iex)
#
# It must be 0x followed by exactly 40 hex characters; anything else is
# refused with nothing installed. The config's Etherbase is then that address,
# and no mining key or password file is made. Set on a re-run or an update, it
# rewrites Etherbase to the address it names and restarts the node; unset, the
# config keeps the Etherbase it has.
#
# The node mines with --miner.threads, on half this computer's logical cores,
# rounded down and at least 1, so the computer stays cool and quiet.
# GHOST_MINE_THREADS chooses the number of cores instead:
#
# curl -fsSL https://get.ghostcoin.com | GHOST_MINE_THREADS=2 sh
#
# (on Windows, in PowerShell: $env:GHOST_MINE_THREADS='2'; irm https://get.ghostcoin.com/win | iex)
#
# It must be a whole number from 1 to 9999; anything else is refused with
# nothing installed. The number is kept in ~/.ghost-node/miner-threads, and a
# re-run or an update keeps it unless GHOST_MINE_THREADS is set again; a node
# installed before the installer knew of it gets the default on its next
# update. Its closing lines say how many cores the node mines on.
#
# The release archive also holds ghost-dash, the Ghost Node Dashboard: a
# small program that shows the node, its mining and its rewards in the
# browser, on 127.0.0.1 only. The installer installs it as
# ~/.ghost-node/bin/ghost-dash and runs it as a service of its own, which also
# starts at boot: the systemd user unit ghost-dashboard.service on Linux, the
# launchd agent com.ghostcoin.dashboard on macOS. It serves on port 8580;
# GHOST_DASH_PORT chooses another, and a re-run or an update keeps the port it
# was installed with unless GHOST_DASH_PORT is set again. When another program
# already listens on 127.0.0.1 at that port, the installer stops with nothing
# installed and says so; the installed dashboard holding it is fine. The
# dashboard signs a browser in with the key in ~/.ghost-node/dashboard-key, 32
# random bytes in hex, made on the first install and kept by re-runs and
# updates. Its link is
#
# http://127.0.0.1:8580/open?key=<the key>
#
# which the closing lines print. On a first install they also open it in the
# browser: with open on macOS, and on Linux with xdg-open when there is a
# desktop session (DISPLAY or WAYLAND_DISPLAY); re-runs and updates only
# print it. ~/.ghost-node/dashboard, a small script that runs ghost-dash open,
# opens it again any time. An update replaces ghost-dash with the copy in the
# newest release's archive, so the release signature covers it too, and
# restarts its service. GHOST_NO_DASHBOARD=1 leaves the dashboard out; set on a
# re-run, it takes out one installed before and keeps its key.
#
# When ~/.ghost-node/mining-paused exists, which the dashboard's pause control
# writes, the node's service starts it without --mine, so the node syncs but
# does not mine, and a pause holds across re-runs, updates and the restarts
# they make. An update finds when the file came or went and rewrites and
# restarts the node's service to match.
#
# The whole installer is the text of installer_source, which the last lines
# run. So a download cut short runs nothing, and the installer can write
# itself out as the update script.
installer_source() {
cat <<'GHOST_INSTALLER'
# The release key: the public half of the key that signs every release. The
# same key is the .pub file next to this script; a test keeps the two equal.
release_key() {
cat <<'KEY'
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA/5nNW/CysxPVl+S5p5VJxi2ywNlEl3HQ1/BzHSAQZ3A=
-----END PUBLIC KEY-----
KEY
}
# The SHA-256 of the release's ghost-verify-<os>-<arch> files. The release
# fills them in, and pins release_url below to itself; until then they are all
# zeros, the placeholder, and the installer refuses to install rather than run
# an unpinned checker.
verifier_hashes() {
cat <<'HASHES'
linux-amd64 0000000000000000000000000000000000000000000000000000000000000000
linux-arm64 0000000000000000000000000000000000000000000000000000000000000000
darwin-amd64 0000000000000000000000000000000000000000000000000000000000000000
darwin-arm64 0000000000000000000000000000000000000000000000000000000000000000
HASHES
}
placeholder_hash=0000000000000000000000000000000000000000000000000000000000000000
release_url=${GHOST_RELEASE_URL:-https://github.com/ghostcoin-core/ghost-node/releases/latest/download}
newest_url=${GHOST_LATEST_URL:-https://github.com/ghostcoin-core/ghost-node/releases/latest/download}
ca_file=${GHOST_INSTALL_CA_FILE:-}
http_port=${GHOST_HTTP_PORT:-8545}
service=ghost-node
update_service=ghost-node-update
label=com.ghostcoin.node
update_label=com.ghostcoin.node.update
dash_service=ghost-dashboard
dash_label=com.ghostcoin.dashboard
dash_default_port=8580
say() {
printf 'ghost-node: %s\n' "$*"
}
die() {
printf 'ghost-node: %s\n' "$*" >&2
exit 1
}
# refuse stops before anything is installed or changed, saying why.
refuse() {
if [ "$mode" = update ]; then
die "$*; refusing to update. Nothing was changed, and the running node was left alone."
fi
die "$*; refusing to install. Nothing was installed."
}
need() {
for tool in "$@"; do
command -v "$tool" >/dev/null 2>&1 || die "this installer needs $tool, which is not on the PATH"
done
}
# platform sets os and arch to this computer's release names.
platform() {
case "$(uname -s)" in
Linux) os=linux ;;
Darwin) os=darwin ;;
*) die "there is no Ghost node release for $(uname -s); releases are for Linux and macOS" ;;
esac
case "$(uname -m)" in
x86_64 | amd64) arch=amd64 ;;
aarch64 | arm64) arch=arm64 ;;
*) die "there is no Ghost node release for $(uname -m) processors; releases are for amd64 and arm64" ;;
esac
}
# sha256 prints the SHA-256 of the file $1, with sha256sum or, on a Mac,
# shasum.
sha256() {
if command -v sha256sum >/dev/null 2>&1; then
s_out=$(sha256sum "$1") || return 1
else
s_out=$(shasum -a 256 "$1") || return 1
fi
s_out=${s_out%% *}
case "$s_out" in
*[!0-9a-f]* | '') return 1 ;;
esac
[ "${#s_out}" = 64 ] || return 1
printf '%s\n' "$s_out"
}
# fetch downloads the https URL $1 to the file $2 with curl, which checks the
# server's certificate against the system's (or GHOST_INSTALL_CA_FILE's) and
# its name, and follows redirects to https URLs only.
fetch() {
case "$1" in
https://*) ;;
*) refuse "will not download $1, which is not an https URL" ;;
esac
f_url=$1
set -- -fsSL --proto =https --proto-redir =https --tlsv1.2 --max-redirs 8 \
--connect-timeout 30 --retry 3 --user-agent ghost-node-installer --output "$2"
if [ -n "$ca_file" ]; then
set -- "$@" --cacert "$ca_file"
fi
curl "$@" "$f_url" 2>"$work/curl.log" ||
refuse "could not download $f_url: $(tail -n 2 "$work/curl.log" | tr '\n' ' ')"
}
# get_verifier downloads this computer's ghost-verify and sets verifier to it,
# once its hash is the one pinned above.
get_verifier() {
v_name="ghost-verify-$os-$arch"
v_pinned=$(verifier_hashes | sed -n "s/^$os-$arch \([0-9a-f]\{64\}\)\$/\1/p")
case "$v_pinned" in
'') refuse "this installer pins no hash for $v_name" ;;
"$placeholder_hash") refuse "this installer has no hash of $v_name pinned yet, only the placeholder, so it cannot check a release" ;;
esac
fetch "$release_url/$v_name" "$work/$v_name"
v_hash=$(sha256 "$work/$v_name") || refuse "could not compute the SHA-256 of $v_name"
[ "$v_hash" = "$v_pinned" ] ||
refuse "the downloaded $v_name has the SHA-256 $v_hash, not the $v_pinned pinned in this installer"
chmod 700 "$work/$v_name"
verifier="$work/$v_name"
}
# verify refuses the archive $1 unless $1.sig is the release key's signature
# of its bytes.
verify() {
release_key >"$work/release.pub"
[ "$(wc -c <"$1.sig" | tr -d ' ')" = 64 ] ||
refuse "the signature of $(basename "$1") is not a 64-byte ed25519 signature"
v_status=0
"$verifier" "$work/release.pub" "$1" "$1.sig" >"$work/verify.log" 2>&1 </dev/null || v_status=$?
case "$v_status" in
0) ;;
1) refuse "the signature of $(basename "$1") does not match the release key" ;;
*) refuse "the signature of $(basename "$1") could not be checked: $(tail -n 2 "$work/verify.log" | tr '\n' ' ')" ;;
esac
}
# run_newest downloads the newest release's install.sh and runs it with
# --update once its signature is the release key's, so that the update moves
# to the newest release and checks it with that release's own pins. The
# newest install.sh, told by GHOST_NEWEST_INSTALLER=1 that it is the newest,
# updates from its own release instead of looking further. This script exits
# with its status.
run_newest() {
newest_url=${newest_url%/}
case "$newest_url" in
https://*) ;;
*) refuse "GHOST_LATEST_URL must be an https URL, not $newest_url" ;;
esac
fetch "$newest_url/install.sh" "$work/install.sh"
fetch "$newest_url/install.sh.sig" "$work/install.sh.sig"
verify "$work/install.sh"
say "the newest release's install.sh is signed by the release key"
n_status=0
GHOST_NEWEST_INSTALLER=1 sh "$work/install.sh" --update </dev/null || n_status=$?
exit "$n_status"
}
# unpack unpacks the verified archive $1 into $work/release.
unpack() {
tar -tzf "$1" >"$work/members" 2>"$work/tar.log" || refuse "the release archive cannot be read"
while IFS= read -r member; do
case "$member" in
/* | ../* | */../* | *..) refuse "the release archive holds an unexpected entry: $member" ;;
esac
done <"$work/members"
mkdir "$work/release"
tar -xzf "$1" -C "$work/release" 2>"$work/tar.log" || refuse "the release archive cannot be unpacked"
if [ ! -f "$work/release/geth" ] || [ -L "$work/release/geth" ]; then
refuse "the release archive holds no geth executable"
fi
chmod 700 "$work/release/geth"
"$work/release/geth" version >/dev/null 2>&1 </dev/null || refuse "the downloaded geth does not run on this computer"
# A release from before the dashboard has no ghost-dash.
has_dash=0
if [ -e "$work/release/ghost-dash" ] || [ -L "$work/release/ghost-dash" ]; then
if [ ! -f "$work/release/ghost-dash" ] || [ -L "$work/release/ghost-dash" ]; then
refuse "the release archive's ghost-dash is not a regular file"
fi
chmod 700 "$work/release/ghost-dash"
has_dash=1
fi
}
# toml_string prints $1 as the inside of a TOML basic string.
toml_string() {
printf '%s' "$1" | sed 's/[\\"]/\\&/g'
}
# config_address prints the mining address of the config.
config_address() {
sed -n 's/^Etherbase = "\(0x[0-9a-fA-F]*\)"$/\1/p' "$config" | head -n 1
}
# set_etherbase rewrites the config's Etherbase to GHOST_MINE_TO's address,
# keeping the rest of the config.
set_etherbase() {
if grep -q '^Etherbase = ' "$config"; then
sed "s/^Etherbase = .*\$/Etherbase = \"$mine_to\"/" "$config" >"$config.new" ||
die "could not rewrite the mining address of $config"
else
{
cat "$config"
printf '\n[Eth.Miner]\nEtherbase = "%s"\n' "$mine_to"
} >"$config.new" || die "could not rewrite the mining address of $config"
fi
mv -f "$config.new" "$config"
say "the config $config now mines to $mine_to"
}
# configure writes the config with GHOST_MINE_TO's address or a new mining
# address, unless there is one; an existing config gets GHOST_MINE_TO's
# address when it is set.
configure() {
if [ -f "$config" ]; then
if [ -n "$mine_to" ]; then
set_etherbase
else
say "keeping the existing config $config"
fi
return 0
fi
if [ -n "$mine_to" ]; then
address=$mine_to
cat >"$config.new" <<EOF
# The Ghost node's config, written by install.sh. The node mines to Etherbase,
# the wallet address GHOST_MINE_TO named, whose key is not on this computer.
# Its HTTP RPC listens on this computer only, for the Ghost web apps.
EOF
else
password=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
[ "${#password}" = 64 ] || die "could not make a password for the mining key"
printf '%s\n' "$password" >"$base/password" || die "could not write the password of the mining key"
created=$("$geth" --datadir "$data" account new --password "$base/password" 2>"$work/account.log" </dev/null) ||
die "could not create the mining key: $(tail -n 3 "$work/account.log" | tr '\n' ' ')"
address=$(printf '%s\n' "$created" | sed -n 's/^Public address of the key: *\(0x[0-9a-fA-F]\{40\}\)$/\1/p')
[ -n "$address" ] || die "could not read the new mining address from: $created"
cat >"$config.new" <<EOF
# The Ghost node's config, written by install.sh. The node mines to Etherbase,
# whose key is in the keystore of DataDir, locked with the password in
# $(toml_string "$base/password"). Its HTTP RPC listens on this computer only,
# for the Ghost web apps.
EOF
fi
cat >>"$config.new" <<EOF
[Eth.Miner]
Etherbase = "$address"
[Node]
DataDir = "$(toml_string "$data")"
HTTPHost = "127.0.0.1"
HTTPPort = $http_port
HTTPModules = ["eth", "net", "web3"]
HTTPCors = ["https://ghost-webapp-alpha.vercel.app", "https://ghost-wallet-nine.vercel.app"]
EOF
mv -f "$config.new" "$config"
}
# cpu_count prints the number of this computer's logical cores, or nothing
# when it cannot tell.
cpu_count() {
c_count=
if [ "$os" = darwin ]; then
c_count=$(sysctl -n hw.logicalcpu 2>/dev/null </dev/null) || c_count=
fi
[ -n "$c_count" ] || c_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null </dev/null) || c_count=
[ -n "$c_count" ] || c_count=$(nproc 2>/dev/null </dev/null) || c_count=
case "$c_count" in
'' | 0* | *[!0-9]*) return 0 ;;
esac
printf '%s\n' "$c_count"
}
# choose_threads sets threads to the number of cores the node mines on:
# GHOST_MINE_THREADS's, else the number the node was installed with, else half
# this computer's logical cores, rounded down and at least 1. It sets
# threads_changed to 1 when that is not the number kept in threads_file.
choose_threads() {
cores=$(cpu_count)
t_kept=
if [ -f "$threads_file" ]; then
t_kept=$(head -n 1 "$threads_file")
case "$t_kept" in
'' | 0* | *[!0-9]* | ?????*) t_kept= ;;
esac
fi
if [ -n "$mine_threads" ]; then
threads=$mine_threads
elif [ -n "$t_kept" ]; then
threads=$t_kept
elif [ -n "$cores" ]; then
threads=$((cores / 2))
[ "$threads" -ge 1 ] || threads=1
else
threads=1
fi
threads_changed=0
[ "$threads" = "$t_kept" ] || threads_changed=1
}
# keep_threads writes the number of cores the node mines on to threads_file.
keep_threads() {
printf '%s\n' "$threads" >"$threads_file.new" || die "could not write $threads_file"
mv -f "$threads_file.new" "$threads_file"
}
# threads_summary prints how many cores the node mines on.
threads_summary() {
if [ -n "$cores" ]; then
printf '%s of the %s logical CPU cores of this computer' "$threads" "$cores"
elif [ "$threads" = 1 ]; then
printf '1 CPU core'
else
printf '%s CPU cores' "$threads"
fi
}
# port_busy succeeds when a program listens on 127.0.0.1:$1. It asks nc, then
# lsof, then makes a connection with curl, which fails with status 7 only when
# nothing listens.
port_busy() {
if command -v nc >/dev/null 2>&1 && nc -z -w 2 127.0.0.1 "$1" >/dev/null 2>&1; then
return 0
fi
if command -v lsof >/dev/null 2>&1 && lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1; then
return 0
fi
p_status=0
curl -s -o /dev/null --noproxy '*' --connect-timeout 2 --max-time 3 "http://127.0.0.1:$1/" >/dev/null 2>&1 </dev/null ||
p_status=$?
[ "$p_status" != 7 ]
}
# own_node_on_port succeeds when the program on 127.0.0.1:$1 is the installed
# node: its HTTP RPC answers eth_coinbase with the Etherbase of the config it
# was started with.
own_node_on_port() {
[ -f "$config" ] || return 1
o_want=$(config_address | tr 'A-F' 'a-f')
[ -n "$o_want" ] || return 1
o_out=$(curl -s --noproxy '*' --connect-timeout 2 --max-time 5 -H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"eth_coinbase","params":[]}' "http://127.0.0.1:$1/" 2>/dev/null </dev/null) ||
return 1
o_got=$(printf '%s\n' "$o_out" | sed -n 's/.*"result" *: *"\(0x[0-9a-fA-F]\{40\}\)".*/\1/p' | head -n 1 | tr 'A-F' 'a-f')
[ -n "$o_got" ] && [ "$o_got" = "$o_want" ]
}
# check_port refuses when another program holds the node's HTTP RPC port: the
# port of the existing config, or GHOST_HTTP_PORT's on a fresh install. The
# installed node itself may hold it, as it does when it runs during an update
# or a re-run.
check_port() {
c_port=$http_port
if [ -f "$config" ]; then
c_conf=$(sed -n 's/^HTTPPort = \([0-9][0-9]*\)$/\1/p' "$config" | head -n 1)
[ -z "$c_conf" ] || c_port=$c_conf
fi
port_busy "$c_port" || return 0
own_node_on_port "$c_port" && return 0
refuse "port $c_port on this computer is already in use by another program; stop it, or install with GHOST_HTTP_PORT=<free port>"
}
# dash_key_value prints the dashboard key, or nothing when there is none.
dash_key_value() {
[ -f "$dash_key" ] || return 0
tr -d ' \t\r\n' <"$dash_key"
}
# own_dash_on_port succeeds when the program on 127.0.0.1:$1 is the installed
# dashboard: its sign-in link with the dashboard's key answers 303 and sets the
# dashboard's session cookie, which only a server holding the key does.
own_dash_on_port() {
d_key=$(dash_key_value)
case "$d_key" in
'' | *[!0-9a-f]*) return 1 ;;
esac
d_head=$(curl -s -o /dev/null -D - --noproxy '*' --connect-timeout 2 --max-time 5 \
"http://127.0.0.1:$1/open?key=$d_key" 2>/dev/null </dev/null) || return 1
printf '%s\n' "$d_head" | head -n 1 | grep -q ' 303' || return 1
printf '%s\n' "$d_head" | grep -qi '^set-cookie: *ghost_dash='
}
# check_dash_port refuses, on an install or a re-run that installs the
# dashboard, when its port is the node's HTTP RPC port or another program
# holds it. The installed dashboard may hold it. check_port sets c_port.
check_dash_port() {
[ "$mode" = install ] && [ "$with_dash" = 1 ] || return 0
[ "$dash_port" != "$c_port" ] ||
refuse "the dashboard's port $dash_port is the node's HTTP RPC port; install with GHOST_DASH_PORT=<another free port>"
port_busy "$dash_port" || return 0
own_dash_on_port "$dash_port" && return 0
refuse "port $dash_port on this computer is already in use by another program; stop it, or install with GHOST_DASH_PORT=<free port>"
}
# initialise writes the genesis of the release's network into a new data
# directory. A release with the network built in has no genesis.json.
initialise() {
if [ -f "$work/release/genesis.json" ] && [ ! -d "$data/geth/chaindata" ]; then
"$geth" --datadir "$data" init "$work/release/genesis.json" >"$work/init.log" 2>&1 </dev/null ||
die "could not initialise the chain: $(tail -n 3 "$work/init.log" | tr '\n' ' ')"
fi
}
# write_updater writes the update script: this installer, run with --update.
write_updater() {
{
printf '#!/bin/sh\n'
printf '#\n# Updates the Ghost node in this directory to the latest signed release.\n'
printf '# install.sh wrote this file: it is the installer run with --update. It\n'
printf '# runs the newest release'\''s install.sh once its signature checks out, and\n'
printf '# that checks the release exactly as the installer does.\n\n'
printf 'installer_source() {\n\tcat <<'\''GHOST_INSTALLER'\''\n'
installer_source
printf 'GHOST_INSTALLER\n}\n\neval "$(installer_source)"\nmain --update "$@" </dev/null\n'
} >"$base/update.new" || die "could not write $base/update"
chmod 700 "$base/update.new"
mv -f "$base/update.new" "$base/update"
}
# unit_word prints $1 as one double-quoted word of a systemd unit line.
unit_word() {
printf '"%s"' "$(printf '%s' "$1" | sed -e 's/[\\"]/\\&/g' -e 's/%/%%/g')"
}
# xml_text prints $1 escaped for an XML text node.
xml_text() {
printf '%s' "$1" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g'
}
# systemd_user succeeds when this user has a systemd user session.
systemd_user() {
command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1
}
# stop_background stops a node this script started without a service.
stop_background() {
stop_pidfile "$base/node.pid" "$geth"
}
# stop_pidfile stops the process whose ID the file $1 holds, when it runs the
# program $2, and removes the file.
stop_pidfile() {
[ -f "$1" ] || return 0
pid=$(cat "$1")
rm -f "$1"
case "$pid" in
'' | *[!0-9]*) return 0 ;;
esac
case "$(ps -p "$pid" -o args= 2>/dev/null)" in
*"$2"*) ;;
*) return 0 ;;
esac
kill "$pid" 2>/dev/null || return 0
tries=0
while kill -0 "$pid" 2>/dev/null && [ "$tries" -lt 60 ]; do
sleep 1
tries=$((tries + 1))
done
}
start_background() {
set -- --config "$config"
[ -e "$paused" ] || set -- "$@" --mine
nohup "$geth" "$@" --miner.threads "$threads" >>"$base/node.log" 2>&1 </dev/null &
echo "$!" >"$base/node.pid"
}
# mine_flag prints the node's --mine argument, with a space before it, or
# nothing while mining is paused.
mine_flag() {
[ -e "$paused" ] || printf ' --mine'
}
# service_mining_changed succeeds when the node's service mines and mining is
# paused, or the other way round.
service_mining_changed() {
case "$os" in
linux)
m_file="$unit_dir/$service.service"
m_pat=' --mine '
;;
darwin)
m_file="$agents/$label.plist"
m_pat='<string>--mine</string>'
;;
esac
[ -f "$m_file" ] || return 1
m_want=1
[ ! -e "$paused" ] || m_want=0
m_have=0
! grep -qF -- "$m_pat" "$m_file" || m_have=1
[ "$m_have" != "$m_want" ]
}
# node_unit prints the node's systemd user unit.
node_unit() {
cat <<EOF
[Unit]
Description=Ghost node
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=$(unit_word "$geth") --config $(unit_word "$config")$(mine_flag) --miner.threads $threads
Restart=on-failure
RestartSec=10
Nice=10
[Install]
WantedBy=default.target
EOF
}
start_linux() {
if systemd_user; then
mkdir -p "$unit_dir"
node_unit >"$unit_dir/$service.service"
if [ "$auto_update" = 1 ]; then
cat >"$unit_dir/$update_service.service" <<EOF
[Unit]
Description=Ghost node update to the latest signed release
[Service]
Type=oneshot
ExecStart=$(unit_word "$base/update")
Nice=10
EOF
cat >"$unit_dir/$update_service.timer" <<EOF
[Unit]
Description=Daily Ghost node update
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
EOF
elif [ -f "$unit_dir/$update_service.timer" ]; then
systemctl --user disable --now "$update_service.timer" >/dev/null 2>&1 || :
rm -f "$unit_dir/$update_service.timer" "$unit_dir/$update_service.service"
fi
stop_background
systemctl --user daemon-reload || die "systemctl --user daemon-reload failed"
systemctl --user enable "$service.service" >/dev/null 2>&1 || die "could not enable the $service user service"
systemctl --user restart "$service.service" || die "could not start the $service user service"
if [ "$auto_update" = 1 ]; then
systemctl --user enable --now "$update_service.timer" >/dev/null 2>&1 ||
die "could not enable the $update_service timer"
update_hint="daily, by the $update_service.timer user timer"
fi
# Lingering keeps the node running after logout and starts it at boot.
if command -v loginctl >/dev/null 2>&1; then
loginctl enable-linger --no-ask-password >/dev/null 2>&1 ||
say "note: the node stops when you log out; 'loginctl enable-linger' keeps it running"
fi
status_hint="systemctl --user status $service"
log_hint="journalctl --user -u $service"
stop_hint="systemctl --user stop $service"
else
say "systemd user services are not available here, so the node runs in the background without one; it does not start again after a reboot until this installer runs again"
stop_background
start_background
if [ "$auto_update" = 1 ]; then
say "without systemd user services updates are not scheduled; run $base/update to update"
fi
status_hint="ps -p \$(cat $base/node.pid)"
log_hint="tail -f $base/node.log"
stop_hint="kill \$(cat $base/node.pid)"
fi
}
# plist_head prints the start of a launchd agent's plist, up to its program
# arguments.
plist_head() {
cat <<EOF
<?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>Label</key>
<string>$1</string>
<key>ProgramArguments</key>
<array>
EOF
}
# reload_agent (re)loads the launchd agent $1 from the plist $2, and fails
# when launchd does not load it. It enables the agent first: launchd refuses to
# bootstrap a disabled one, and stopping the node with launchctl disable leaves
# it disabled.
reload_agent() {
launchctl bootout "$domain/$1" >/dev/null 2>&1 || :
launchctl enable "$domain/$1" >/dev/null 2>&1 || :
# bootout returns before the old agent is gone; bootstrap until it loads.
tries=0
until launchctl bootstrap "$domain" "$2" 2>"$work/launchctl.log"; do
tries=$((tries + 1))
[ "$tries" -lt 10 ] || return 1
sleep 1
done
}
# load_agent (re)loads the launchd agent $1 from the plist $2, or stops saying
# why launchd did not load it.
load_agent() {
reload_agent "$1" "$2" && return 0
l_disabled=$(launchctl print-disabled "$domain" 2>/dev/null | grep -F "\"$1\"" | sed 's/^[[:space:]]*//' | tr '\n' ' ')
die "could not start the launchd agent $1: $(cat "$work/launchctl.log").${l_disabled:+ launchd lists it as: ${l_disabled% }.} Run 'launchctl enable $domain/$1', then run this installer again."
}
# node_plist prints the node's launchd agent.
node_plist() {
plist_head "$label"
cat <<EOF
<string>$(xml_text "$geth")</string>
<string>--config</string>
<string>$(xml_text "$config")</string>
EOF
[ -e "$paused" ] || printf '\t\t<string>--mine</string>\n'
cat <<EOF
<string>--miner.threads</string>
<string>$threads</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>Nice</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>$(xml_text "$base/node.log")</string>
<key>StandardErrorPath</key>
<string>$(xml_text "$base/node.log")</string>
</dict>
</plist>
EOF
}
start_darwin() {
mkdir -p "$agents"
plist="$agents/$label.plist"
update_plist="$agents/$update_label.plist"
node_plist >"$plist"
load_agent "$label" "$plist"
if [ "$auto_update" = 1 ]; then
{
plist_head "$update_label"
cat <<EOF
<string>$(xml_text "$base/update")</string>
</array>
<key>StartInterval</key>
<integer>86400</integer>
<key>RunAtLoad</key>
<false/>
<key>Nice</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>$(xml_text "$base/update.log")</string>
<key>StandardErrorPath</key>
<string>$(xml_text "$base/update.log")</string>
</dict>
</plist>
EOF
} >"$update_plist"
load_agent "$update_label" "$update_plist"
update_hint="daily, by the launchd agent $update_label"
elif [ -f "$update_plist" ]; then
launchctl bootout "$domain/$update_label" >/dev/null 2>&1 || :
rm -f "$update_plist"
fi
status_hint="launchctl print $domain/$label"
log_hint="tail -f $base/node.log"
stop_hint="launchctl bootout $domain/$label"
}
# refresh_service rewrites the installed node's systemd unit or launchd agent
# to mine on $threads cores, for restart_node to start it so. A node started
# without a service gets them when start_background starts it again.
refresh_service() {
case "$os" in
linux)
[ -f "$unit_dir/$service.service" ] || return 0
node_unit >"$unit_dir/$service.service.new" || die "could not rewrite the $service user service"
mv -f "$unit_dir/$service.service.new" "$unit_dir/$service.service"
;;
darwin)
[ -f "$agents/$label.plist" ] || return 0
node_plist >"$agents/$label.plist.new" || die "could not rewrite the launchd agent $label"
mv -f "$agents/$label.plist.new" "$agents/$label.plist"
;;
esac
service_rewritten=1
}
# restart_node restarts the installed node, however it was started. After
# refresh_service it has the service manager read the node's service again.
restart_node() {
case "$os" in
linux)
if [ -f "$unit_dir/$service.service" ] && systemd_user; then
if [ "$service_rewritten" = 1 ]; then
systemctl --user daemon-reload || return 1
fi
systemctl --user restart "$service.service"
else
stop_background
start_background
fi
;;
darwin)
if [ "$service_rewritten" = 1 ]; then
reload_agent "$label" "$agents/$label.plist"
return
fi
launchctl kickstart -k "$domain/$label" >/dev/null 2>&1 || {
launchctl enable "$domain/$label" >/dev/null 2>&1 || :
launchctl bootstrap "$domain" "$agents/$label.plist" >/dev/null 2>&1
}
;;
esac
}
# sh_word prints $1 as one single-quoted word of a shell script.
sh_word() {
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
}
# kept_dash_port prints the port the installed dashboard was installed with,
# from its open script, or nothing.
kept_dash_port() {
[ -f "$dash_script" ] || return 0
sed -n 's/^exec .* --port \([0-9][0-9]*\) "\$@"$/\1/p' "$dash_script" | head -n 1
}
# make_dash_key writes the dashboard key unless there is one, and sets
# dash_new to 1 when it made it: the dashboard's first install.
make_dash_key() {
dash_new=0
[ -z "$(dash_key_value)" ] || return 0
k_value=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
[ "${#k_value}" = 64 ] || die "could not make a key for the dashboard"
printf '%s\n' "$k_value" >"$dash_key.new" || die "could not write $dash_key"
chmod 600 "$dash_key.new"
mv -f "$dash_key.new" "$dash_key"
dash_new=1
}
# write_dash_script writes the script that opens the dashboard signed in.
write_dash_script() {
{
printf '#!/bin/sh\n#\n'
printf '# Opens the Ghost Node Dashboard in the browser, signed in: it prints the\n'
printf '# dashboard'\''s link and opens it. install.sh wrote this file.\n\n'
printf 'exec %s open --node-dir %s --port %s "$@"\n' "$(sh_word "$dash")" "$(sh_word "$base")" "$dash_port"
} >"$dash_script.new" || die "could not write $dash_script"
chmod 700 "$dash_script.new"
mv -f "$dash_script.new" "$dash_script"
}
# dash_unit prints the dashboard's systemd user unit.
dash_unit() {
cat <<EOF
[Unit]
Description=Ghost Node Dashboard
[Service]
ExecStart=$(unit_word "$dash") --node-dir $(unit_word "$base") --port $dash_port
Restart=on-failure
RestartSec=10
Nice=10
[Install]
WantedBy=default.target
EOF
}
# dash_plist prints the dashboard's launchd agent.
dash_plist() {
plist_head "$dash_label"
cat <<EOF
<string>$(xml_text "$dash")</string>
<string>--node-dir</string>
<string>$(xml_text "$base")</string>
<string>--port</string>
<string>$dash_port</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>Nice</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>$(xml_text "$base/dashboard.log")</string>
<key>StandardErrorPath</key>
<string>$(xml_text "$base/dashboard.log")</string>
</dict>
</plist>
EOF
}
start_dash_background() {
nohup "$dash" --node-dir "$base" --port "$dash_port" >>"$base/dashboard.log" 2>&1 </dev/null &
echo "$!" >"$base/dashboard.pid"
}
# start_dashboard writes the dashboard's service and (re)starts it, as the
# node's is: a systemd user unit, a launchd agent, or without systemd user
# services a process in the background.
start_dashboard() {
case "$os" in
linux)
if systemd_user; then
mkdir -p "$unit_dir"
dash_unit >"$unit_dir/$dash_service.service"
stop_pidfile "$base/dashboard.pid" "$dash"
systemctl --user daemon-reload || die "systemctl --user daemon-reload failed"
systemctl --user enable "$dash_service.service" >/dev/null 2>&1 ||
die "could not enable the $dash_service user service"
systemctl --user restart "$dash_service.service" || die "could not start the $dash_service user service"
else
stop_pidfile "$base/dashboard.pid" "$dash"
start_dash_background
fi
;;
darwin)
mkdir -p "$agents"
dash_plist >"$agents/$dash_label.plist"
load_agent "$dash_label" "$agents/$dash_label.plist"
;;
esac
}
# restart_dashboard restarts the installed dashboard, however it was started.
restart_dashboard() {
case "$os" in
linux)
if [ -f "$unit_dir/$dash_service.service" ] && systemd_user; then
systemctl --user restart "$dash_service.service"
else
stop_pidfile "$base/dashboard.pid" "$dash"
start_dash_background
fi
;;
darwin)
launchctl kickstart -k "$domain/$dash_label" >/dev/null 2>&1 || {
launchctl enable "$domain/$dash_label" >/dev/null 2>&1 || :
launchctl bootstrap "$domain" "$agents/$dash_label.plist" >/dev/null 2>&1
}
;;
esac
}
# remove_dashboard takes out a dashboard installed before, for
# GHOST_NO_DASHBOARD=1, and keeps its key.
remove_dashboard() {
r_had=0
case "$os" in
linux)
if [ -f "$unit_dir/$dash_service.service" ]; then
systemctl --user disable --now "$dash_service.service" >/dev/null 2>&1 || :
rm -f "$unit_dir/$dash_service.service"
r_had=1
fi
;;