@3d-print-shop/installer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/install.sh +592 -0
- package/package.json +32 -0
package/install.sh
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# AIDEV-NOTE: this exists because the shop refuses to make its own data directory, and that refusal is
|
|
5
|
+
# deliberate - a service that creates whatever path it was pointed at puts a queue somewhere nobody
|
|
6
|
+
# is looking, and a typo is then work lost rather than a shop that will not start. See
|
|
7
|
+
# design/3d-print-shop.md, "The installer creates the root; the service never does". So the two
|
|
8
|
+
# directories, the user that owns them, and the supervisor that starts the process are this file's.
|
|
9
|
+
#
|
|
10
|
+
# Nothing here writes a credential. `init` does that, as the service user, and it is the one step
|
|
11
|
+
# that has to be a person's - the token it answers with exists nowhere else.
|
|
12
|
+
|
|
13
|
+
LABEL=com.dcorbin.3d-print-shop
|
|
14
|
+
# AIDEV-NOTE: three places rather than one, because the three things a shop keeps are three kinds and
|
|
15
|
+
# the system says where each goes: work awaiting processing, state that outlives a restart, and a
|
|
16
|
+
# claim that must not. The service works these out itself from `systemLayout` - what the installer
|
|
17
|
+
# does is MAKE the two that have to be there before it starts. The third is /var/run, which is
|
|
18
|
+
# emptied by a boot and so is the shop's own to create every time.
|
|
19
|
+
JOBS=/var/spool/3d-print-shop/jobs
|
|
20
|
+
STATE=/var/lib/3d-print-shop
|
|
21
|
+
ETC=/etc/3d-print-shop
|
|
22
|
+
OUT_LOG=/var/log/3d-print-shop.log
|
|
23
|
+
ERR_LOG=/var/log/3d-print-shop.err.log
|
|
24
|
+
|
|
25
|
+
# AIDEV-NOTE: the shop is COPIED here rather than run out of a checkout, because the service user is
|
|
26
|
+
# not the developer and a home directory is not theirs to walk into - `/Users/<somebody>` is 0750 on
|
|
27
|
+
# macOS, so a daemon pointed at a checkout inside one cannot read a byte of it. Copying also means a
|
|
28
|
+
# `git checkout` of another branch is not a live change to the running service.
|
|
29
|
+
INSTALL_DIR=/usr/local/lib/3d-print-shop
|
|
30
|
+
|
|
31
|
+
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
32
|
+
|
|
33
|
+
# AIDEV-NOTE: two ways this file is reached, and the difference is who put the code somewhere the
|
|
34
|
+
# service can read. Exploded by npm, the server is a sibling in the same scope and npm has already
|
|
35
|
+
# resolved everything it needs - so there is nothing to copy and copying would be a second, staler
|
|
36
|
+
# tree. In a checkout there is a build, sitting where a daemon cannot get at it.
|
|
37
|
+
case "$HERE" in
|
|
38
|
+
*/node_modules/@3d-print-shop/installer) MODE=package ;;
|
|
39
|
+
*) MODE=checkout ;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
if [ "$MODE" = package ]; then
|
|
43
|
+
BUILT="$HERE/../server/dist/main.js"
|
|
44
|
+
SERVER=$BUILT
|
|
45
|
+
MANIFEST="$HERE/package.json"
|
|
46
|
+
BUILT_PAGE="$HERE/../ui/dist"
|
|
47
|
+
PAGE=$BUILT_PAGE
|
|
48
|
+
else
|
|
49
|
+
REPO=$(cd "$HERE/../.." && pwd)
|
|
50
|
+
BUILT="$REPO/packages/server/dist/main.js"
|
|
51
|
+
SERVER="$INSTALL_DIR/packages/server/dist/main.js"
|
|
52
|
+
MANIFEST="$REPO/package.json"
|
|
53
|
+
BUILT_PAGE="$REPO/packages/ui/dist"
|
|
54
|
+
PAGE="$INSTALL_DIR/packages/ui/dist"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# AIDEV-NOTE: always said, because the page is not optional - `requirePage` has already refused an
|
|
58
|
+
# install without one. It used to be left out when the directory was missing, which is how a shop
|
|
59
|
+
# that served nobody anything came up looking like a successful install.
|
|
60
|
+
pageArguments() {
|
|
61
|
+
printf ' <string>--page</string>\n <string>%s</string>\n' "$PAGE"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pageOption() {
|
|
65
|
+
printf ' --page %s' "$PAGE"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
PLIST="/Library/LaunchDaemons/$LABEL.plist"
|
|
69
|
+
UNIT=/etc/systemd/system/3d-print-shop.service
|
|
70
|
+
|
|
71
|
+
case "$(uname -s)" in
|
|
72
|
+
Darwin) PLATFORM=macos; SHOP_USER=_printshop; ROOT_GROUP=wheel ;;
|
|
73
|
+
Linux) PLATFORM=linux; SHOP_USER=printshop; ROOT_GROUP=root ;;
|
|
74
|
+
*) echo "3d-print-shop installs on macOS and Linux, not $(uname -s)" >&2; exit 1 ;;
|
|
75
|
+
esac
|
|
76
|
+
SHOP_GROUP=$SHOP_USER
|
|
77
|
+
|
|
78
|
+
say() { printf '%s\n' "$*"; }
|
|
79
|
+
refuse() { printf '%s\n' "$*" >&2; exit 1; }
|
|
80
|
+
|
|
81
|
+
usage() {
|
|
82
|
+
cat <<USAGE
|
|
83
|
+
usage: sudo $0 [install|update|uninstall]
|
|
84
|
+
|
|
85
|
+
install make the data and credentials directories, the user that owns them, the copy of the
|
|
86
|
+
shop the service runs, and the service itself
|
|
87
|
+
update copy a fresh build over the installed one and restart - what to run after 'yarn build',
|
|
88
|
+
and only from a checkout: an installed package is updated by installing it again
|
|
89
|
+
uninstall stop the service and remove it - the data, the credentials and the user are left,
|
|
90
|
+
because they hold work and secrets this script did not create
|
|
91
|
+
USAGE
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Named as the person would type it, which is the bin npm put on their PATH once there is one.
|
|
95
|
+
asTyped() {
|
|
96
|
+
if [ "$MODE" = package ]; then printf '3d-print-shop-install %s' "$1"; else printf '%s %s' "$0" "$1"; fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
requireRoot() {
|
|
100
|
+
[ "$(id -u)" = 0 ] || refuse "this writes under /var, /etc and /usr/local, so it needs sudo: sudo $(asTyped "$1")"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# AIDEV-NOTE: the page is REQUIRED, and this is where that is enforced. The shop is installed for a
|
|
104
|
+
# person to use and the page is how a person uses it - it is where somebody logs in, watches a
|
|
105
|
+
# machine and gives a verdict on what came off the bed. A shop answering only its API is a shop
|
|
106
|
+
# nobody in the workshop can do any of that with, so it is refused rather than installed and
|
|
107
|
+
# mentioned. Shaped like `requireBuild` below, and for the same reason: each mode is told the remedy
|
|
108
|
+
# it actually has.
|
|
109
|
+
requirePage() {
|
|
110
|
+
[ -d "$BUILT_PAGE" ] && return 0
|
|
111
|
+
|
|
112
|
+
if [ "$MODE" = package ]; then
|
|
113
|
+
refuse "$BUILT_PAGE is not there - @3d-print-shop/ui is not installed beside this"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
refuse "$BUILT_PAGE is not there - run 'yarn install && yarn build' in $REPO first, which builds it"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
requireBuild() {
|
|
120
|
+
if [ ! -f "$BUILT" ]; then
|
|
121
|
+
if [ "$MODE" = package ]; then
|
|
122
|
+
refuse "$BUILT is not there - @3d-print-shop/server is not installed beside this"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
refuse "$BUILT is not there - run 'yarn install && yarn build' in $REPO first"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# AIDEV-NOTE: the same rule the node has to pass, and for the same reason - an npm run under a
|
|
129
|
+
# version manager explodes its global packages inside the home directory, which is the one place
|
|
130
|
+
# the service user cannot walk into. Checked here rather than left to fail at boot with a daemon
|
|
131
|
+
# that starts, finds nothing, and is restarted for ever.
|
|
132
|
+
if [ "$MODE" = package ]; then
|
|
133
|
+
case "$HERE" in
|
|
134
|
+
/Users/* | /home/* | "$HOME"/*)
|
|
135
|
+
refuse "this is installed under $HERE, and the service runs as $SHOP_USER, which cannot read
|
|
136
|
+
into a home directory. Install it with a node that lives outside one (brew install node@24, or your
|
|
137
|
+
distribution's package), so that npm puts its global packages outside one too"
|
|
138
|
+
;;
|
|
139
|
+
esac
|
|
140
|
+
fi
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# The floor whichever manifest is in play already declares, so this script says it nowhere: the
|
|
144
|
+
# repo's when run from a checkout, the installed package's own when npm exploded one. The ceiling in
|
|
145
|
+
# `engines` is the package manager's business; what matters here is not running the service on
|
|
146
|
+
# something older than the code was written for.
|
|
147
|
+
requiredNode() {
|
|
148
|
+
local floor
|
|
149
|
+
floor=$(sed -n 's/.*"node" *: *">=\([0-9][0-9.]*\).*/\1/p' "$MANIFEST" | head -1)
|
|
150
|
+
|
|
151
|
+
# Said rather than shrugged at: a floor that came back empty would let every node past the check,
|
|
152
|
+
# which is the version guard silently not being one.
|
|
153
|
+
[ -n "$floor" ] || refuse "$MANIFEST does not say which node this runs on, as engines.node"
|
|
154
|
+
|
|
155
|
+
printf '%s\n' "$floor"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
isAtLeast() {
|
|
159
|
+
[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# AIDEV-NOTE: a node under somebody's home directory is the one that cannot work here, and it is
|
|
163
|
+
# also the one a developer has - a version manager installs per user, and the daemon runs as
|
|
164
|
+
# somebody else. Found rather than assumed, and refused with the fix rather than installed into a
|
|
165
|
+
# service that would never start.
|
|
166
|
+
findNode() {
|
|
167
|
+
local wanted=$1 candidate version found=''
|
|
168
|
+
local major=${wanted%%.*}
|
|
169
|
+
|
|
170
|
+
for candidate in \
|
|
171
|
+
${PRINT_SHOP_NODE:-} \
|
|
172
|
+
"/opt/homebrew/opt/node@$major/bin/node" \
|
|
173
|
+
/opt/homebrew/bin/node \
|
|
174
|
+
"/usr/local/opt/node@$major/bin/node" \
|
|
175
|
+
/usr/local/bin/node \
|
|
176
|
+
/usr/bin/node \
|
|
177
|
+
"$(command -v node || true)"; do
|
|
178
|
+
if [ -z "$candidate" ] || [ ! -x "$candidate" ]; then continue; fi
|
|
179
|
+
case "$candidate" in /Users/* | /home/* | "$HOME"/*) continue ;; esac
|
|
180
|
+
|
|
181
|
+
version=$("$candidate" -v 2>/dev/null | tr -d v)
|
|
182
|
+
if isAtLeast "$version" "$wanted"; then
|
|
183
|
+
printf '%s\n' "$candidate"
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
found="$found
|
|
188
|
+
$candidate (v$version)"
|
|
189
|
+
done
|
|
190
|
+
|
|
191
|
+
refuse "no node of $wanted or newer outside a home directory.${found:+ What is here:$found}
|
|
192
|
+
|
|
193
|
+
The shop runs as $SHOP_USER, which cannot read a node a version manager installed under yours.
|
|
194
|
+
Install one system-wide (brew install node@$major, or your distribution's package), or say
|
|
195
|
+
PRINT_SHOP_NODE=/path/to/node"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
madeDirectory() {
|
|
199
|
+
local directory=$1
|
|
200
|
+
|
|
201
|
+
mkdir -p "$directory"
|
|
202
|
+
chown "$SHOP_USER:$SHOP_GROUP" "$directory"
|
|
203
|
+
|
|
204
|
+
# 0700 and nothing looser: a job directory another user could rename away is a print that silently
|
|
205
|
+
# never happens, and the keys under /etc open the printers directly.
|
|
206
|
+
chmod 700 "$directory"
|
|
207
|
+
say " $directory ($SHOP_USER, 0700)"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# What the service reads, so it is root's and readable by everybody - the opposite of the two
|
|
211
|
+
# directories above, and for the opposite reason: there is nothing secret in it, and the one thing
|
|
212
|
+
# that must not happen is the user the service runs as being able to rewrite its own code.
|
|
213
|
+
copiedIn() {
|
|
214
|
+
local what=$1 into="$INSTALL_DIR/$2"
|
|
215
|
+
|
|
216
|
+
mkdir -p "$into"
|
|
217
|
+
rm -rf "${into:?}/$(basename "$what")"
|
|
218
|
+
cp -R "$what" "$into/"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
copiedTheBuild() {
|
|
222
|
+
local package
|
|
223
|
+
|
|
224
|
+
copiedIn "$REPO/package.json" ''
|
|
225
|
+
for package in client server; do
|
|
226
|
+
copiedIn "$REPO/packages/$package/package.json" "packages/$package"
|
|
227
|
+
copiedIn "$REPO/packages/$package/dist" "packages/$package"
|
|
228
|
+
done
|
|
229
|
+
|
|
230
|
+
# The page. Files rather than a module: the shop is pointed at the directory and told nothing about
|
|
231
|
+
# what is in it. Not conditional - `requirePage` has already refused an install without one.
|
|
232
|
+
copiedIn "$BUILT_PAGE" 'packages/ui'
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# AIDEV-NOTE: node_modules whole, symlinks and all. Yarn links a workspace as a RELATIVE symlink
|
|
236
|
+
# (`@3d-print-shop/client -> ../../packages/client`), so a copy that keeps the layout keeps the
|
|
237
|
+
# links working, and `cp -R` copies a symlink as a symlink rather than following it.
|
|
238
|
+
copiedEverything() {
|
|
239
|
+
copiedTheBuild
|
|
240
|
+
copiedIn "$REPO/node_modules" ''
|
|
241
|
+
droppedTheLinksThatLeadNowhere
|
|
242
|
+
|
|
243
|
+
chown -R "root:$ROOT_GROUP" "$INSTALL_DIR"
|
|
244
|
+
chmod -R a+rX,go-w "$INSTALL_DIR"
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# AIDEV-NOTE: only the packages the service RUNS are copied, so a workspace link to one left behind -
|
|
248
|
+
# the octoprint-sim the tests print against - arrives pointing at nothing. Nothing resolves it at
|
|
249
|
+
# runtime, and it is removed anyway: a link that leads nowhere is not part of an install, and the
|
|
250
|
+
# next person to look in here should not have to work out whether it matters.
|
|
251
|
+
#
|
|
252
|
+
# Just this directory, because a workspace is the only thing yarn links rather than copies.
|
|
253
|
+
droppedTheLinksThatLeadNowhere() {
|
|
254
|
+
local link
|
|
255
|
+
|
|
256
|
+
for link in "$INSTALL_DIR/node_modules/@3d-print-shop"/*; do
|
|
257
|
+
if [ -L "$link" ] && [ ! -e "$link" ]; then rm -f "$link"; fi
|
|
258
|
+
done
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------- macOS
|
|
262
|
+
|
|
263
|
+
freeSystemId() {
|
|
264
|
+
local used candidate
|
|
265
|
+
used=$( (dscl . -list /Users UniqueID; dscl . -list /Groups PrimaryGroupID) | awk '{print $2}')
|
|
266
|
+
|
|
267
|
+
# 200-400 is where macOS keeps its own daemon users, and a uid below 500 is hidden from the login
|
|
268
|
+
# window without anything else having to say so.
|
|
269
|
+
for candidate in $(seq 200 400); do
|
|
270
|
+
grep -qx "$candidate" <<<"$used" || { printf '%s\n' "$candidate"; return 0; }
|
|
271
|
+
done
|
|
272
|
+
|
|
273
|
+
refuse 'every system id between 200 and 400 is taken'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
madeServiceUserOnMacos() {
|
|
277
|
+
if dscl . -read "/Users/$SHOP_USER" >/dev/null 2>&1; then
|
|
278
|
+
say " $SHOP_USER is already here"
|
|
279
|
+
return 0
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
local id
|
|
283
|
+
id=$(freeSystemId)
|
|
284
|
+
|
|
285
|
+
dscl . -create "/Groups/$SHOP_GROUP"
|
|
286
|
+
dscl . -create "/Groups/$SHOP_GROUP" PrimaryGroupID "$id"
|
|
287
|
+
dscl . -create "/Groups/$SHOP_GROUP" RealName '3D Print Shop'
|
|
288
|
+
dscl . -create "/Groups/$SHOP_GROUP" Password '*'
|
|
289
|
+
|
|
290
|
+
dscl . -create "/Users/$SHOP_USER"
|
|
291
|
+
dscl . -create "/Users/$SHOP_USER" UniqueID "$id"
|
|
292
|
+
dscl . -create "/Users/$SHOP_USER" PrimaryGroupID "$id"
|
|
293
|
+
dscl . -create "/Users/$SHOP_USER" RealName '3D Print Shop'
|
|
294
|
+
dscl . -create "/Users/$SHOP_USER" NFSHomeDirectory /var/empty
|
|
295
|
+
dscl . -create "/Users/$SHOP_USER" UserShell /usr/bin/false
|
|
296
|
+
dscl . -create "/Users/$SHOP_USER" IsHidden 1
|
|
297
|
+
dscl . -create "/Users/$SHOP_USER" Password '*'
|
|
298
|
+
|
|
299
|
+
say " $SHOP_USER (uid $id), which owns the data directory and can be logged in as by nobody"
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
wroteLog() {
|
|
303
|
+
local file=$1
|
|
304
|
+
|
|
305
|
+
touch "$file"
|
|
306
|
+
chown "$SHOP_USER:$SHOP_GROUP" "$file"
|
|
307
|
+
chmod 640 "$file"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
wrotePlist() {
|
|
311
|
+
local node=$1
|
|
312
|
+
|
|
313
|
+
# AIDEV-NOTE: KeepAlive only where it did NOT exit cleanly. `3d-print-shop shutdown` is an
|
|
314
|
+
# operator asking it to stop, and a plain KeepAlive would start it again a second later - which is
|
|
315
|
+
# a shop that cannot be stopped without unloading the daemon.
|
|
316
|
+
cat >"$PLIST" <<PLIST
|
|
317
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
318
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
319
|
+
<plist version="1.0">
|
|
320
|
+
<dict>
|
|
321
|
+
<key>Label</key><string>$LABEL</string>
|
|
322
|
+
<key>ProgramArguments</key>
|
|
323
|
+
<array>
|
|
324
|
+
<string>$node</string>
|
|
325
|
+
<string>$SERVER</string>
|
|
326
|
+
<string>serve</string>
|
|
327
|
+
$(pageArguments) </array>
|
|
328
|
+
<key>UserName</key><string>$SHOP_USER</string>
|
|
329
|
+
<key>GroupName</key><string>$SHOP_GROUP</string>
|
|
330
|
+
<key>WorkingDirectory</key><string>$STATE</string>
|
|
331
|
+
<key>RunAtLoad</key><true/>
|
|
332
|
+
<key>KeepAlive</key>
|
|
333
|
+
<dict>
|
|
334
|
+
<key>SuccessfulExit</key><false/>
|
|
335
|
+
</dict>
|
|
336
|
+
<key>StandardOutPath</key><string>$OUT_LOG</string>
|
|
337
|
+
<key>StandardErrorPath</key><string>$ERR_LOG</string>
|
|
338
|
+
</dict>
|
|
339
|
+
</plist>
|
|
340
|
+
PLIST
|
|
341
|
+
|
|
342
|
+
# launchd refuses a plist anybody but root can write to.
|
|
343
|
+
chown "root:$ROOT_GROUP" "$PLIST"
|
|
344
|
+
chmod 644 "$PLIST"
|
|
345
|
+
say " $PLIST"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------- Linux
|
|
349
|
+
|
|
350
|
+
madeServiceUserOnLinux() {
|
|
351
|
+
if getent passwd "$SHOP_USER" >/dev/null; then
|
|
352
|
+
say " $SHOP_USER is already here"
|
|
353
|
+
return 0
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
useradd --system --user-group --home-dir /nonexistent --shell /usr/sbin/nologin "$SHOP_USER"
|
|
357
|
+
say " $SHOP_USER, which owns the data directory and can be logged in as by nobody"
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
wroteUnit() {
|
|
361
|
+
local node=$1
|
|
362
|
+
|
|
363
|
+
# Restart=on-failure rather than always, for the reason the plist keeps KeepAlive conditional:
|
|
364
|
+
# `3d-print-shop shutdown` is somebody asking it to stop. stdout is the shop's log and journald is
|
|
365
|
+
# what captures it, so there is no file here to rotate.
|
|
366
|
+
cat >"$UNIT" <<UNIT
|
|
367
|
+
[Unit]
|
|
368
|
+
Description=3D Print Shop
|
|
369
|
+
After=network-online.target
|
|
370
|
+
Wants=network-online.target
|
|
371
|
+
|
|
372
|
+
[Service]
|
|
373
|
+
ExecStart=$node $SERVER serve$(pageOption)
|
|
374
|
+
ExecReload=/bin/kill -HUP \$MAINPID
|
|
375
|
+
User=$SHOP_USER
|
|
376
|
+
Group=$SHOP_GROUP
|
|
377
|
+
WorkingDirectory=$STATE
|
|
378
|
+
Restart=on-failure
|
|
379
|
+
RestartSec=5
|
|
380
|
+
KillSignal=SIGTERM
|
|
381
|
+
StandardOutput=journal
|
|
382
|
+
StandardError=journal
|
|
383
|
+
|
|
384
|
+
[Install]
|
|
385
|
+
WantedBy=multi-user.target
|
|
386
|
+
UNIT
|
|
387
|
+
|
|
388
|
+
chmod 644 "$UNIT"
|
|
389
|
+
say " $UNIT"
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
# ---------------------------------------------------------------------------- starting and stopping
|
|
393
|
+
|
|
394
|
+
startIt() {
|
|
395
|
+
if [ "$PLATFORM" = macos ]; then
|
|
396
|
+
launchctl bootout "system/$LABEL" 2>/dev/null || true
|
|
397
|
+
launchctl enable "system/$LABEL"
|
|
398
|
+
launchctl bootstrap system "$PLIST"
|
|
399
|
+
else
|
|
400
|
+
systemctl daemon-reload
|
|
401
|
+
systemctl enable --now 3d-print-shop
|
|
402
|
+
fi
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
stopIt() {
|
|
406
|
+
if [ "$PLATFORM" = macos ]; then
|
|
407
|
+
launchctl bootout "system/$LABEL" 2>/dev/null || true
|
|
408
|
+
else
|
|
409
|
+
systemctl stop 3d-print-shop 2>/dev/null || true
|
|
410
|
+
fi
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
isInstalled() {
|
|
414
|
+
if [ "$PLATFORM" = macos ]; then [ -f "$PLIST" ]; else [ -f "$UNIT" ]; fi
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
restarting() {
|
|
418
|
+
if [ "$PLATFORM" = macos ]; then
|
|
419
|
+
printf 'sudo launchctl kickstart -k system/%s' "$LABEL"
|
|
420
|
+
else
|
|
421
|
+
printf 'sudo systemctl restart 3d-print-shop'
|
|
422
|
+
fi
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
reloading() {
|
|
426
|
+
if [ "$PLATFORM" = macos ]; then
|
|
427
|
+
printf 'sudo launchctl kill HUP system/%s' "$LABEL"
|
|
428
|
+
else
|
|
429
|
+
printf 'sudo systemctl reload 3d-print-shop'
|
|
430
|
+
fi
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
reading() {
|
|
434
|
+
if [ "$PLATFORM" = macos ]; then
|
|
435
|
+
printf 'tail -f %s' "$OUT_LOG"
|
|
436
|
+
else
|
|
437
|
+
printf 'journalctl -u 3d-print-shop -f'
|
|
438
|
+
fi
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# ---------------------------------------------------------------------------- what it does
|
|
442
|
+
|
|
443
|
+
install() {
|
|
444
|
+
requireRoot install
|
|
445
|
+
requireBuild
|
|
446
|
+
requirePage
|
|
447
|
+
|
|
448
|
+
local wanted node
|
|
449
|
+
wanted=$(requiredNode)
|
|
450
|
+
node=$(findNode "$wanted")
|
|
451
|
+
|
|
452
|
+
say 'the user it runs as:'
|
|
453
|
+
if [ "$PLATFORM" = macos ]; then madeServiceUserOnMacos; else madeServiceUserOnLinux; fi
|
|
454
|
+
|
|
455
|
+
say 'what it keeps its work and its credentials in:'
|
|
456
|
+
madeDirectory "$JOBS"
|
|
457
|
+
madeDirectory "$STATE"
|
|
458
|
+
madeDirectory "$ETC"
|
|
459
|
+
|
|
460
|
+
say 'the shop itself:'
|
|
461
|
+
stopIt
|
|
462
|
+
if [ "$MODE" = package ]; then
|
|
463
|
+
say " $(cd "$HERE/.." && pwd) (where npm put it)"
|
|
464
|
+
else
|
|
465
|
+
copiedEverything
|
|
466
|
+
say " $INSTALL_DIR (root, and readable by everybody - there is nothing secret in it)"
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
say " $PAGE (the page it serves beside the API)"
|
|
470
|
+
|
|
471
|
+
say 'what supervises it:'
|
|
472
|
+
if [ "$PLATFORM" = macos ]; then
|
|
473
|
+
wroteLog "$OUT_LOG"
|
|
474
|
+
wroteLog "$ERR_LOG"
|
|
475
|
+
wrotePlist "$node"
|
|
476
|
+
else
|
|
477
|
+
wroteUnit "$node"
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
# AIDEV-NOTE: NOT started where there are no callers yet. Every route names its caller, so a shop
|
|
481
|
+
# with none refuses to start - and a supervisor would then restart it every few seconds for as
|
|
482
|
+
# long as the machine was up, which is a log nobody can read and a fault that reads like a bug.
|
|
483
|
+
if [ ! -f "$ETC/callers.json" ]; then
|
|
484
|
+
cat <<NEXT
|
|
485
|
+
|
|
486
|
+
Installed, and not started: this shop has no callers yet, and one with none refuses to start.
|
|
487
|
+
|
|
488
|
+
Give it its first admin, as the user it runs as - the token is said once and kept nowhere else:
|
|
489
|
+
|
|
490
|
+
sudo -u $SHOP_USER $node $SERVER init <your-name>
|
|
491
|
+
|
|
492
|
+
Then keep that token where the client looks for it, as yourself:
|
|
493
|
+
|
|
494
|
+
mkdir -p ~/.config/3d-print-shop && chmod 700 ~/.config/3d-print-shop
|
|
495
|
+
printf %s '<the token>' > ~/.config/3d-print-shop/token
|
|
496
|
+
chmod 600 ~/.config/3d-print-shop/token
|
|
497
|
+
|
|
498
|
+
Each printer's API key goes in $ETC/printer-keys.json as {"mk4": "..."}, 0600 and owned by
|
|
499
|
+
$SHOP_USER. Then run this again to start it:
|
|
500
|
+
|
|
501
|
+
sudo $(asTyped install)
|
|
502
|
+
NEXT
|
|
503
|
+
return 0
|
|
504
|
+
fi
|
|
505
|
+
|
|
506
|
+
startIt
|
|
507
|
+
|
|
508
|
+
cat <<RUNNING
|
|
509
|
+
|
|
510
|
+
Installed and running, on http://localhost:7373 - loopback, which is where a token travelling in
|
|
511
|
+
the clear belongs.
|
|
512
|
+
|
|
513
|
+
its log $(reading)
|
|
514
|
+
a new build $(if [ "$MODE" = package ]; then printf 'sudo npm i -g @3d-print-shop/installer'; else printf 'sudo %s update' "$0"; fi)
|
|
515
|
+
a credential edit a file in $ETC, then $(reloading)
|
|
516
|
+
RUNNING
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
update() {
|
|
520
|
+
requireRoot update
|
|
521
|
+
requireBuild
|
|
522
|
+
requirePage
|
|
523
|
+
[ "$MODE" = checkout ] || refuse 'an installed package is updated by installing it again: sudo npm i -g @3d-print-shop/installer'
|
|
524
|
+
isInstalled || refuse "there is no service to update - run 'sudo $(asTyped install)' first"
|
|
525
|
+
|
|
526
|
+
stopIt
|
|
527
|
+
copiedTheBuild
|
|
528
|
+
chown -R "root:$ROOT_GROUP" "$INSTALL_DIR"
|
|
529
|
+
chmod -R a+rX,go-w "$INSTALL_DIR"
|
|
530
|
+
startIt
|
|
531
|
+
|
|
532
|
+
say "the build in $REPO is now the one running - $(reading) to see it come up"
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
uninstall() {
|
|
536
|
+
requireRoot uninstall
|
|
537
|
+
|
|
538
|
+
stopIt
|
|
539
|
+
if [ "$PLATFORM" = macos ]; then
|
|
540
|
+
rm -f "$PLIST"
|
|
541
|
+
say "stopped, and $PLIST is gone"
|
|
542
|
+
else
|
|
543
|
+
rm -f "$UNIT"
|
|
544
|
+
systemctl daemon-reload
|
|
545
|
+
say "stopped, and $UNIT is gone"
|
|
546
|
+
fi
|
|
547
|
+
|
|
548
|
+
if [ -d "$INSTALL_DIR" ]; then
|
|
549
|
+
rm -rf "$INSTALL_DIR"
|
|
550
|
+
say "$INSTALL_DIR is gone"
|
|
551
|
+
fi
|
|
552
|
+
|
|
553
|
+
# AIDEV-NOTE: the data holds work nobody has judged and /etc holds every token the shop knows.
|
|
554
|
+
# Neither is this script's to throw away on the way out - uninstalling a service is not the same
|
|
555
|
+
# act as discarding what it was holding, and one of them cannot be undone.
|
|
556
|
+
cat <<KEPT
|
|
557
|
+
|
|
558
|
+
Left alone, because they hold work and secrets rather than installation:
|
|
559
|
+
|
|
560
|
+
$JOBS the work it was holding
|
|
561
|
+
$STATE its printers and sessions
|
|
562
|
+
$ETC its callers and its printer keys
|
|
563
|
+
$SHOP_USER the user that owns both
|
|
564
|
+
|
|
565
|
+
Remove them yourself if that is what you mean.
|
|
566
|
+
KEPT
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
case "${1:-install}" in
|
|
570
|
+
# AIDEV-NOTE: what npm's postinstall calls, and it is a separate word from `install` because yarn
|
|
571
|
+
# runs it on every install in the CHECKOUT too, where nobody asked for a daemon and there is
|
|
572
|
+
# nothing to explode. It also must not fail: a postinstall that exits non-zero fails the whole npm
|
|
573
|
+
# install, so a person who has not used sudo is told what to type rather than shouted at.
|
|
574
|
+
#
|
|
575
|
+
# npm drops to the owner of its prefix when it is run as root, so being root here is not something
|
|
576
|
+
# to count on even under sudo.
|
|
577
|
+
from-npm)
|
|
578
|
+
[ "$MODE" = package ] || exit 0
|
|
579
|
+
if [ "$(id -u)" != 0 ]; then
|
|
580
|
+
say "3d-print-shop is unpacked. It makes a system user, a data directory and a daemon, so the install
|
|
581
|
+
itself is one more command: sudo 3d-print-shop-install"
|
|
582
|
+
exit 0
|
|
583
|
+
fi
|
|
584
|
+
|
|
585
|
+
install
|
|
586
|
+
;;
|
|
587
|
+
install | --install) install ;;
|
|
588
|
+
update | --update) update ;;
|
|
589
|
+
uninstall | --uninstall) uninstall ;;
|
|
590
|
+
-h | --help | help) usage ;;
|
|
591
|
+
*) usage >&2; exit 1 ;;
|
|
592
|
+
esac
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@3d-print-shop/installer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Installs 3d-print-shop as a service: the user that owns its work, the directories it keeps it in, and launchd or systemd to run it",
|
|
6
|
+
"author": "dcorbin",
|
|
7
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/dcorbin-apps/3d-print-shop.git",
|
|
11
|
+
"directory": "packages/installer"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"3d-print-shop-install": "./install.sh"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"install.sh"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"postinstall": "./install.sh from-npm",
|
|
21
|
+
"build": "echo \"nothing to build - this package is a shell script\" && exit 0",
|
|
22
|
+
"typecheck": "echo \"no typescript here\" && exit 0",
|
|
23
|
+
"test": "echo \"tests run from root\" && exit 0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@3d-print-shop/server": "1.0.0",
|
|
27
|
+
"@3d-print-shop/ui": "1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24.16 <25"
|
|
31
|
+
}
|
|
32
|
+
}
|