Repository navigation
Expand file tree
/
Copy pathinstall-linux.sh
More file actions
executable file
·606 lines (508 loc) · 22.4 KB
/
Copy pathinstall-linux.sh
File metadata and controls
executable file
·606 lines (508 loc) · 22.4 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
#!/bin/bash
# Deadlock API Ingest - Linux Installation Script
# This script downloads and installs the deadlock-api-ingest application as a systemd user service
# -E: the ERR trap below also fires inside functions, where all the work runs.
set -Eeuo pipefail
# --- Configuration ---
APP_NAME="deadlock-api-ingest"
GITHUB_REPO="deadlock-api/deadlock-api-ingest"
# The latest release's assets, by name; no API call or JSON parsing needed.
RELEASE_URL="https://github.com/$GITHUB_REPO/releases/latest/download"
MAIN_ASSET="${APP_NAME}-ubuntu-latest"
# Installation paths (User-level, no root required)
INSTALL_DIR="$HOME/.local/share/$APP_NAME"
# Outside INSTALL_DIR, which the old installation's uninstaller deletes, and on the same
# filesystem, so the checked binary can be moved into place.
DOWNLOAD_PATH="$HOME/.local/share/.$APP_NAME-download"
BIN_DIR="$HOME/.local/bin"
FINAL_EXECUTABLE_NAME=$APP_NAME
# Opens Deadlock's memory read-only for the service; installed root-owned with cap_sys_ptrace.
HELPER_ASSET="${APP_NAME}-memgrant-linux"
HELPER_PATH="/usr/local/libexec/$APP_NAME/${APP_NAME}-memgrant"
# Service and logging
SERVICE_NAME=$APP_NAME
# Per user: a fixed path in /tmp left by one user (or a sudo run) can't be written by the next.
LOG_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/$APP_NAME/install.log"
SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
SYSTEMD_SERVICE_FILE="$SYSTEMD_USER_DIR/${SERVICE_NAME}.service"
# --- Colors for output ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# --- Helper Functions ---
# Function to write to log and console
log() {
local level="$1"
local message="$2"
local timestamp color
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
"ERROR") color="$RED" ;;
"WARN") color="$YELLOW" ;;
"SUCCESS") color="$GREEN" ;;
"INFO") color="$BLUE" ;;
*) color="$NC" ;;
esac
local log_message="[$timestamp] [$level] $message"
# --- FIX APPLIED HERE ---
# All console output is redirected to stderr (>&2) to prevent it from being
# captured by command substitutions like `var=$(...)`.
echo -e "${color}${log_message}${NC}" >&2
# The log file is written to separately and is unaffected.
mkdir -p "$(dirname "$LOG_FILE")"
echo "$log_message" >> "$LOG_FILE"
}
# Function to execute commands quietly while logging details
execute_quietly() {
local cmd=("$@")
# Execute command and capture both stdout and stderr to log file
if "${cmd[@]}" >> "$LOG_FILE" 2>&1; then
return 0
else
local exit_code=$?
log "ERROR" "Command failed: ${cmd[*]}"
return $exit_code
fi
}
# Downloads $1 to $2, with wget where curl isn't installed (Ubuntu's desktop install has no curl).
# $2 is replaced only once the download succeeds: wget empties its output file on a 404.
fetch() {
local part="$2.part"
if command -v curl >/dev/null 2>&1; then
curl -fsSL --user-agent "Bash-Installer" -o "$part" "$1" 2>> "$LOG_FILE"
else
wget -q --user-agent="Bash-Installer" -O "$part" "$1" 2>> "$LOG_FILE"
fi && mv -f "$part" "$2" && return 0
echo "Download failed: $1" >> "$LOG_FILE"
rm -f "$part"
return 1
}
# Function to check for required dependencies
check_dependencies() {
if command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1; then
log "SUCCESS" "All dependencies are already installed."
return 0
fi
# Either curl or wget is enough; suggest curl.
local missing_deps=(curl)
log "WARN" "Neither curl nor wget is installed."
log "INFO" "Install one with your package manager, for example:"
if command -v apt-get >/dev/null 2>&1; then
log "INFO" " sudo apt-get install ${missing_deps[*]}"
elif command -v dnf >/dev/null 2>&1; then
log "INFO" " sudo dnf install ${missing_deps[*]}"
elif command -v yum >/dev/null 2>&1; then
log "INFO" " sudo yum install ${missing_deps[*]}"
elif command -v pacman >/dev/null 2>&1; then
log "INFO" " sudo pacman -S ${missing_deps[*]}"
elif command -v apk >/dev/null 2>&1; then
log "INFO" " sudo apk add ${missing_deps[*]}"
elif command -v zypper >/dev/null 2>&1; then
log "INFO" " sudo zypper install ${missing_deps[*]}"
else
log "INFO" " Use your system's package manager to install: ${missing_deps[*]}"
fi
log "ERROR" "Cannot proceed without required dependencies."
exit 1
}
# Function to download file
download_file() {
local url="$1"
local output_path="$2"
mkdir -p "$(dirname "$output_path")"
# Log detailed info to file, show simple progress to user
echo "Downloading from: $url" >> "$LOG_FILE"
echo "Saving to: $output_path" >> "$LOG_FILE"
if ! fetch "$url" "$output_path"; then
log "ERROR" "Download failed (details in $LOG_FILE)."
rm -f "$output_path"
exit 1
fi
log "SUCCESS" "Download complete."
}
# Runs the downloaded binary once, so one this system can't run (e.g. too old a glibc) stops
# the install before the current installation is removed.
check_binary_runs() {
local path="$1"
chmod +x "$path"
local version_output line
if ! version_output=$("$path" --version 2>&1); then
log "ERROR" "The downloaded binary does not run on this system:"
while IFS= read -r line; do log "ERROR" " $line"; done <<< "$version_output"
if [[ "$version_output" == *GLIBC_* ]]; then
log "INFO" "It needs a newer glibc than this system has; a newer release of your distribution has one."
fi
rm -f "$path"
exit 1
fi
}
# Installs the app icon into the user's icon theme, so desktop entries can use Icon=$APP_NAME.
# Best effort: entries without it still work, just with a generic icon.
install_icon() {
local icon_dir="$HOME/.local/share/icons/hicolor/256x256/apps"
local icon_url="https://raw.githubusercontent.com/$GITHUB_REPO/master/assets/icon.png"
if ! mkdir -p "$icon_dir" 2>/dev/null || ! fetch "$icon_url" "$icon_dir/$APP_NAME.png"; then
log "WARN" "Could not install the app icon; shortcuts will use a generic icon."
return 1
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -q -t "$HOME/.local/share/icons/hicolor" 2>> "$LOG_FILE" || true
fi
return 0
}
# Function to create desktop shortcut
create_desktop_shortcut() {
local executable_path="$1"
local arguments="${2:-}"
local shortcut_name="${3:-Deadlock API Ingest}"
local comment="${4:-Monitors Steam cache for Deadlock match replays}"
local file_name="${5:-}"
log "INFO" "Creating desktop shortcut: $shortcut_name..."
# Use the current user's local applications directory
local desktop_dir="$HOME/.local/share/applications"
# Create the directory if it doesn't exist
if ! mkdir -p "$desktop_dir" 2>/dev/null; then
log "WARN" "Could not create applications directory. Desktop shortcut will not be created."
return 1
fi
# Create a safe filename from the shortcut name
# The file name is also the app id: Wayland desktops match a window's app_id to the
# desktop file name, X11 ones its WM_CLASS to StartupWMClass (set to the same below).
local safe_name="$file_name"
if [[ -z "$safe_name" ]]; then
safe_name=$(echo "$shortcut_name" | tr ' ' '-' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]//g')
fi
local desktop_file="$desktop_dir/${safe_name}.desktop"
# Build the Exec line with arguments if provided
local exec_line="$executable_path"
if [[ -n "$arguments" ]]; then
exec_line="$executable_path $arguments"
fi
# Create the .desktop file
cat > "$desktop_file" << EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=$shortcut_name
Comment=$comment
Exec=$exec_line
Icon=$APP_NAME
Terminal=false
Categories=Utility;
Keywords=deadlock;api;ingest;steam;matches;replay;tracker;statlocker;
StartupWMClass=$safe_name
EOF
chmod 644 "$desktop_file"
# Try to update desktop database if available
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$desktop_dir" 2>> "$LOG_FILE" || true
fi
log "SUCCESS" "Desktop shortcut created at: $desktop_file"
return 0
}
# Function to manage the systemd service
manage_service() {
local action="$1"
case "$action" in
"remove")
if systemctl --user is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
execute_quietly systemctl --user stop "$SERVICE_NAME"
fi
if systemctl --user is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then
execute_quietly systemctl --user disable "$SERVICE_NAME"
fi
if [[ -f "$SYSTEMD_SERVICE_FILE" ]]; then
rm -f "$SYSTEMD_SERVICE_FILE"
fi
systemctl --user daemon-reload 2>> "$LOG_FILE" || true
;;
"create")
local executable_path="$2"
# Create systemd user directory if it doesn't exist
mkdir -p "$SYSTEMD_USER_DIR"
cat > "$SYSTEMD_SERVICE_FILE" << EOF
[Unit]
Description=Deadlock API Ingest - Monitors Steam cache for match replays
Documentation=https://github.com/deadlock-api/deadlock-api-ingest
[Service]
Type=simple
ExecStart=$executable_path
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=$SERVICE_NAME
# Security Hardening (no NoNewPrivileges: it would keep the memory helper from its capability)
PrivateTmp=true
[Install]
WantedBy=default.target
EOF
chmod 644 "$SYSTEMD_SERVICE_FILE"
systemctl --user daemon-reload 2>> "$LOG_FILE"
log "SUCCESS" "User service created."
;;
"start")
execute_quietly systemctl --user enable "$SERVICE_NAME"
execute_quietly systemctl --user start "$SERVICE_NAME"
sleep 3
if systemctl --user is-active --quiet "$SERVICE_NAME"; then
log "SUCCESS" "Application service started successfully."
else
log "ERROR" "Service failed to start. Please check the logs."
log "INFO" "To check status: systemctl --user status $SERVICE_NAME"
log "INFO" "To view logs: journalctl --user -u $SERVICE_NAME -n 50"
exit 1
fi
;;
esac
}
# Installs the helper that lets the service read match salts from Deadlock's memory. Most
# distributions only let a program read another's memory if it started it, and Steam starts
# the game, so the helper gets that one right (cap_sys_ptrace) and is kept root-owned.
install_memory_helper() {
if [[ "$(cat /proc/sys/kernel/yama/ptrace_scope 2>/dev/null || echo 0)" == "0" ]]; then
log "INFO" "This system lets the service read Deadlock's memory without a helper."
return 0
fi
local elevate
if [[ -r /dev/tty ]] && command -v sudo >/dev/null 2>&1; then
elevate=sudo
elif command -v pkexec >/dev/null 2>&1; then
elevate=pkexec
else
log "WARN" "Neither sudo nor pkexec found; skipping the memory helper."
return 0
fi
log "INFO" "To also read match salts from Deadlock's memory while it runs, a small helper is"
log "INFO" "installed to $HELPER_PATH. It only opens Deadlock's memory, read-only and for your"
log "INFO" "user, and needs your password once ($elevate)."
if [[ -r /dev/tty ]]; then
local response=""
echo -n "Install the memory helper? (Y/n): " >&2
read -t 30 -r response < /dev/tty || true
if [[ "${response,,}" == n* ]]; then
log "INFO" "Skipped. Starting the game with '$APP_NAME -- %command%' in Steam also works."
return 0
fi
fi
local helper_tmp
helper_tmp=$(mktemp)
if ! fetch "$RELEASE_URL/$HELPER_ASSET" "$helper_tmp"; then
log "WARN" "Could not download the memory helper; skipping it."
rm -f "$helper_tmp"
return 0
fi
# shellcheck disable=SC2016 # expanded by the elevated shell
if "$elevate" /bin/sh -c 'install -D -o root -g root -m 0755 "$1" "$2" && { setcap cap_sys_ptrace=ep "$2" || { rm -f "$2"; exit 1; }; }' \
sh "$helper_tmp" "$HELPER_PATH" 2>> "$LOG_FILE"; then
log "SUCCESS" "Memory helper installed."
else
log "WARN" "Could not install the memory helper (setcap comes with libcap2-bin or libcap)."
log "INFO" "Starting the game with '$APP_NAME -- %command%' in Steam also works."
fi
rm -f "$helper_tmp"
}
# Function to prompt user for auto-start setup
prompt_for_autostart() {
# Check if we're running in an interactive terminal
if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
log "INFO" "Non-interactive mode detected. Enabling auto-start by default."
return 0
fi
log "INFO" "Auto-start will automatically start the service when the system boots."
log "INFO" "This ensures the application is always running in the background."
echo >&2
local attempts=0
local max_attempts=2
while [[ $attempts -lt $max_attempts ]]; do
echo -n "Would you like to enable auto-start on system boot? (y/n): " >&2
local response
if read -t 10 -r response; then
case "${response,,}" in
y|yes)
return 0
;;
n|no)
return 1
;;
*)
attempts=$((attempts + 1))
if [[ $attempts -lt $max_attempts ]]; then
echo "Invalid response. Please enter 'y' for yes or 'n' for no." >&2
fi
;;
esac
else
log "INFO" "No response received within 10 seconds. Enabling auto-start by default."
return 0
fi
done
log "INFO" "Maximum attempts reached. Enabling auto-start by default."
return 0
}
# --- Main Installation Logic ---
main() {
# Everything goes into the home of the user running the script, and the service is that
# user's systemd service, so under sudo it would all be root's.
if [[ $EUID -eq 0 ]]; then
echo -e "${RED}Run the installer as your own user: run the same command again without sudo.${NC}" >&2
echo "It installs for the user who runs it and asks for your password itself if it needs it." >&2
exit 1
fi
log "INFO" "Starting Deadlock API Ingest installation..."
log "INFO" "Log file is available at: $LOG_FILE"
check_dependencies
download_file "$RELEASE_URL/$MAIN_ASSET" "$DOWNLOAD_PATH"
check_binary_runs "$DOWNLOAD_PATH"
# Try to run uninstall script if it exists (clean uninstall before fresh install)
local existing_uninstall_script="$INSTALL_DIR/uninstall-linux.sh"
if [[ -f "$existing_uninstall_script" ]]; then
log "INFO" "Found existing installation. Running uninstall script..."
if "$existing_uninstall_script" --silent >> "$LOG_FILE" 2>&1; then
log "SUCCESS" "Previous installation uninstalled successfully."
else
log "WARN" "Uninstall script failed, continuing with manual cleanup."
fi
fi
# Remove existing service (in case uninstall script didn't exist or failed)
manage_service "remove"
# Clean up any remaining processes
killall "$APP_NAME" 2>/dev/null || true
# Ensure installation directory exists
mkdir -p "$INSTALL_DIR"
local final_executable_path="$INSTALL_DIR/$FINAL_EXECUTABLE_NAME"
mv "$DOWNLOAD_PATH" "$final_executable_path"
# Create bin directory if it doesn't exist
mkdir -p "$BIN_DIR"
local bin_symlink="$BIN_DIR/$FINAL_EXECUTABLE_NAME"
ln -sf "$final_executable_path" "$bin_symlink"
log "SUCCESS" "Application installed successfully."
# Download uninstall script
log "INFO" "Downloading uninstall script..."
local uninstall_script_path="$INSTALL_DIR/uninstall-linux.sh"
local uninstall_script_url="https://raw.githubusercontent.com/deadlock-api/deadlock-api-ingest/master/uninstall-linux.sh"
if fetch "$uninstall_script_url" "$uninstall_script_path"; then
chmod +x "$uninstall_script_path"
log "SUCCESS" "Uninstall script downloaded to: $uninstall_script_path"
else
log "WARN" "Failed to download uninstall script, but continuing installation."
log "INFO" "You can manually download it from: $uninstall_script_url"
fi
install_memory_helper
# Create the main service (but don't enable/start it yet)
manage_service "create" "$final_executable_path"
# Prompt user for auto-start setup
if prompt_for_autostart; then
manage_service "start"
log "SUCCESS" "Auto-start enabled. The service will start automatically on user login."
else
log "INFO" "Auto-start disabled. You can start the service manually with: systemctl --user start $SERVICE_NAME"
log "INFO" "To enable auto-start later, run: systemctl --user enable $SERVICE_NAME"
# Offer to create a desktop shortcut instead
echo >&2
log "INFO" "Would you like to create a desktop shortcut instead?"
local create_shortcut=false
local attempts=0
local max_attempts=2
# Check if we're running in an interactive terminal
if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
log "INFO" "Non-interactive mode detected. Creating desktop shortcut by default."
create_shortcut=true
else
while [[ $attempts -lt $max_attempts ]]; do
echo -n "Create desktop shortcut? (y/n): " >&2
local response
if read -t 10 -r response; then
case "${response,,}" in
y|yes)
create_shortcut=true
break
;;
n|no)
create_shortcut=false
break
;;
*)
attempts=$((attempts + 1))
if [[ $attempts -lt $max_attempts ]]; then
echo "Invalid response. Please enter 'y' for yes or 'n' for no." >&2
fi
;;
esac
else
log "INFO" "No response received within 10 seconds. Creating desktop shortcut by default."
create_shortcut=true
break
fi
done
if [[ $attempts -ge $max_attempts ]]; then
log "INFO" "Maximum attempts reached. Creating desktop shortcut by default."
create_shortcut=true
fi
fi
if [[ "$create_shortcut" == true ]]; then
# Create main shortcut
local main_created=false
local once_created=false
if create_desktop_shortcut "$final_executable_path" "" "Deadlock API Ingest (Background)" "Monitors Steam cache for Deadlock match replays" "$APP_NAME"; then
main_created=true
fi
# Create "once" shortcut for initial cache ingest only
if create_desktop_shortcut "$final_executable_path" "--once" "Deadlock API Ingest (Once)" "Scan existing Steam cache once and exit"; then
once_created=true
fi
if [[ "$main_created" == true && "$once_created" == true ]]; then
log "INFO" "Desktop shortcuts created:"
log "INFO" " - Deadlock API Ingest: Start the application with monitoring"
log "INFO" " - Deadlock API Ingest (Once): Run once to ingest existing cache files only"
log "INFO" "You can also start manually: $final_executable_path"
elif [[ "$main_created" == true ]]; then
log "INFO" "Main desktop shortcut created. You can also run: $final_executable_path"
else
log "INFO" "You can start the application by running: $final_executable_path"
fi
else
log "INFO" "You can manually start the application by running: $final_executable_path"
log "INFO" "To run once (ingest existing cache only): $final_executable_path --once"
fi
fi
# The app-menu entry everyone gets (search "Deadlock"): opens the status window, which
# also starts the background service if it isn't running.
# Neither is required for the tool to work, so failures only warn.
install_icon || true
if ! create_desktop_shortcut "$final_executable_path" "--gui" "Deadlock API Ingest" "See the Deadlock matches you have sent to trackers" "$APP_NAME-status"; then
log "WARN" "Could not create the app menu entry. Open the status window with: $final_executable_path --gui"
fi
log "SUCCESS" "🚀 Deadlock API Ingest has been installed successfully!"
# The final messages should also be sent to stderr to not interfere with any potential scripting.
{
echo
echo -e "${GREEN}Installation complete.${NC}"
echo
if systemctl --user is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then
echo -e "${GREEN}[+] Auto-start is enabled${NC} - The service will start automatically on user login."
else
echo -e "${YELLOW}[-] Auto-start is disabled${NC} - The service will not start automatically on user login."
echo -e " To enable auto-start: ${YELLOW}systemctl --user enable $SERVICE_NAME${NC}"
fi
echo
echo -e "You can manage the main service with the following commands:"
echo -e " - Check status: ${YELLOW}systemctl --user status $SERVICE_NAME${NC}"
echo -e " - View logs: ${YELLOW}journalctl --user -u $SERVICE_NAME -f${NC}"
echo -e " - Log files: ${YELLOW}\${XDG_DATA_HOME:-\$HOME/.local/share}/$APP_NAME/logs/${NC}"
echo -e " - Stop service: ${YELLOW}systemctl --user stop $SERVICE_NAME${NC}"
echo -e " - Start service: ${YELLOW}systemctl --user start $SERVICE_NAME${NC}"
if ! systemctl --user is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then
echo -e " - Enable auto-start: ${YELLOW}systemctl --user enable $SERVICE_NAME${NC}"
fi
echo
echo -e "To uninstall, run: ${YELLOW}$INSTALL_DIR/uninstall-linux.sh${NC}"
echo
} >&2
}
# Graceful error handling
trap 'log "ERROR" "An unexpected error occurred at line $LINENO. Installation failed."' ERR
# Run the main function
main "$@"