summaryrefslogtreecommitdiff
path: root/session-common.sh.inc
blob: 165cdd91084394a6bac1f0fb3301cbb6db7f7dfb (plain)
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
# Parabola Install Wizard - common functions
#
# Copyright (C) 2020,2022 bill-auger <bill-auger@programmer.net>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This file is part of Parabola Install Wizard.
#
# Parabola Install Wizard is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Parabola Install Wizard is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Parabola Install Wizard. If not, see <http://www.gnu.org/licenses/>.


cd $(dirname ${BASH_SOURCE[0]})
source ./constants.sh.inc


## sanity checks ##

if   (( ! IN_CHROOT ))
then which dialog &> /dev/null || ! echo "${DIALOG_ERR_MSG}"    || exit
     (( ! $EUID ))             || ! echo "${PRIVILEGE_ERR_MSG}" || exit
     [[ -w ./.session_state ]] || ! echo "${STATEFILE_ERR_MSG}" || exit
fi


## state helpers ##

SetStateVar() # (var_name value*)
{
  local var_name=$1 ; shift ;
  local value="$*"

  sed -i "/^${var_name}=.*/d"    ./.session_state
  echo "${var_name}=${value}" >> ./.session_state
}

GetStateVar() # (var_name [def_value])
{
  local var_name=$1
  local def_value=$2
  local stored_value=$(grep "${var_name}=" ./.session_state | cut -d '=' -f 2)

  if   [[ -n "${stored_value}" ]] && [[ "${stored_value}" != '_UNDEFINED_' ]]
  then echo ${stored_value}
  else echo ${def_value}
  fi
}

AreConfiguredStateVars() # ( state_var [ state_var ] ... )
{
# for state_var in $* ; do echo -n "AreConfiguredStateVars() state_var=${state_var}=$(GetStateVar ${state_var})=" ; [[ -n "$(GetStateVar ${state_var})" ]] && echo "OK" || ! echo "NFG" ; done ;

  # TODO: replace each call to this function with proper validations
  for state_var in $* ; do [[ -n "$(GetStateVar ${state_var})" ]] || return 1 ; done ;

  return 0
}


## dialog prompt helpers ##

# WizardDlg() notes:
# the primary purpose of the WizardDlg() function, is to simplify and normalize
#   creation of various dialog boxes, such that they can all be handled uniformly.
#
# in order for this to work properly, callers of WizardDlg() should pass the
# box-type argument (eg: --menu, --yesno, --inputbox) as positional parameter #2,
# followed by it's box-specific options. additional dialog options may be set
# via the 'DIALOGOPTS' environment variable.
#
# when the "Cancel" button or <ESC> key is pressed, in a 'msgbox',
#   'dialog' exits with a non-zero status; and no result is printed.
# install.sh runs under `set -e`; so we can not propogate the non-zero status,
#   otherwise, all callers of these helpers would need to supress it.
# callers should instead, exit upon the empty result, when a selection is mandatory.
#
# because an 'inputbox' may be blank, when the "OK" button is pressed, we emit a space,
#   in order to maintain the invariant, such that the "OK" result is never empty.
#
# 'dialog' is quirky; and it was difficult to acheive complete uniformity.
# so unfortunately, there are some caveats:
# * 'yesno' dialogs exit with a non-zero status upon the "No" button.
#   the "Cancel" and "No" buttons are identical, and indistinguishable at run-time.
#   the $YESNO_YES or $YESNO_NO values are printed explicitly for 'yesno' dialogs,
#   so that callers can handle the 'yesno' dialogs in the same way as 'msgbox' dialogs.
# * 'checklist' dialogs print no result if nothing was selected,
#   which would be indistinguishable from a cancel operation (eg: <ESC> key).
#   for that reason, $CANCEL_SENTINEL will be printed explicitly, upon a cancel operation
#   for 'checklist' dialogs, so that interested callers may handle it.
readonly CANCEL_STATUS=1
readonly CANCEL_SENTINEL='CANCEL'

# dialog prompt for session-init.sh (with auto-accept timeout)
InitDlg() # (title prompt default_option options*)
{
  local title="$1"
  local prompt="$2"
  local default_option="$3"
  local options=("${@:4}")

  dialog --stdout --sleep 1 --no-tags --no-cancel --timeout 30 \
         --backtitle    "${title}"                             \
         --default-item "${default_option}"                    \
         --menu         "${prompt}" 15 42 10                   \
         "${options[@]}"
}

# dialog prompts for install.sh
WizardDlg() # ("title" box_type "boxargs"* options*)
{
  local title="$1"
  local dialog_type=$2
  local boxargs_and_options=("${@:3}")
  local opts=( $( [[ "${dialog_type}" == '--progressbox' ]] ||
                  [[ "${dialog_type}" == '--infobox'     ]] || echo '--erase-on-exit' ) )
  local result
  local status

  result=$( DIALOG_ESC=${CANCEL_STATUS}                       \
            dialog --stdout --insecure --sleep 1              \
                   --colors ${opts[*]}                        \
                   --backtitle    "${TR[wizard-${Lang}]}"     \
                   --title        "${title}"                  \
                   ${dialog_type} "${boxargs_and_options[@]}" ) && status=$? || status=$?

  # NOTE: a failure on the following LOC, indicates a bug in the caller code
  (( ! status || status == CANCEL_STATUS ))

  if   [[ "${dialog_type}" == '--menu' ]]
  then echo "${result}"

  elif [[ "${dialog_type}" == '--yesno' ]]
  then echo $(( ! status ))

  elif [[ "${dialog_type}" == '--inputbox'    ]] ||
       [[ "${dialog_type}" == '--passwordbox' ]]
  then if   (( status != CANCEL_STATUS ))
       then [[ -n "${result}" ]] && echo "${result}" || echo ' '
       fi

  elif [[ "${dialog_type}" == '--checklist' ]]
  then (( status != CANCEL_STATUS )) && echo "${result}" || echo ${CANCEL_SENTINEL}

  elif [[ "${dialog_type}" == '--infobox'     ]] ||
       [[ "${dialog_type}" == '--gauge'       ]] ||
       [[ "${dialog_type}" == '--msgbox'      ]] ||
       [[ "${dialog_type}" == '--progressbox' ]]
  then echo ''

  else echo "unhandled dialog_type: ${dialog_type}" >&2
  fi

  (( status == CANCEL_STATUS )) && status=0 || true ;

  return ${status}
}

ExitFail() # ("exit_msg")
{
  WizardDlg "${TR[exit_fail-${Lang}]}" --infobox "$(echo -e $@)" 0 0
  exit
}


## input validation helpers ##

ValidateId() # (login)
{
  local login="$*"

  [[ "${login}" =~ ^[[:space:]]*[^[:space:]]+[[:space:]]*$ ]] # TODO: improve this
}


## partitioning helpers ##

readonly DISK_RECORD_SEP='|'
readonly DISK_FIELD_SEP=':'

declare -A PartsData # raw data
declare -a DlgParams # dialog options

UnmountAll()
{
  mkdir -p /mnt
  umount /mnt/boot &> /dev/null || true
  umount /mnt/home &> /dev/null || true
  umount /mnt      &> /dev/null || true
}

GetDisksPartsData()
{
  local line
  local dev
  local size
  local part_n
  local format

  # example parted output (GPT):
  #   BYT;
  #   /dev/sda:1074MB:scsi:512:512:gpt:ATA QEMU HARDDISK:; # QEMU device
  #   1:1049kB:106MB:105MB:ext4::;                         # EXT4 partition
  #   2:106MB:211MB:105MB:::;                              # un-formatted partition
  #   3:211MB:316MB:105MB:::swap;                          # un-formatted swap partition
  #   4:316MB:421MB:105MB:linux-swap(v1)::swap;            # formatted swap partition
  #                                                        # <-- ~600MB unallocated space
  #
  # example parted output (MBR):
  #   BYT;
  #   /dev/sdb:250GB:scsi:512:512:msdos:ATA ST3250318AS:; # hardware device
  #   1:1049kB:525MB:524MB:ntfs::boot;                    # windows boot partition
  #   2:525MB:20.5GB:20.0GB:ntfs::;                       # windows system partition
  #   4:20.5GB:249GB:228GB:::lvm;                         # LLVM partition
  #   3:249GB:250GB:1049MB:ntfs::msftres;                 # windows data partition
  #
  # NOTE: the example /dev/sdb is a typical windows partition scheme, where partition #2 was shrunken,
  #       partition #3 was deleted, and a *nix partition (#4) was added
  #
  # example parted output (multiple devices, no partition table on sdb):
  #   BYT;
  #   /dev/sda:42.9GB:scsi:512:512:msdos:ATA QEMU HARDDISK:;
  #   1:1049kB:42.9GB:42.9GB:ext4::;
  #
  #   BYT;                                                                      
  #   /dev/sdb:1074MB:scsi:512:512:unknown:ATA QEMU HARDDISK:;
  #
  # NOTE: in the 'BYT;' line of the example /dev/sdb,
  #       all of those spaces and a stray CR are present in the output
  #       '                                                                      \rBYT;'
  #
  # example GetDisksPartsData() output:
  #   /dev/sda:1074MB|/dev/sda1:105MB:ext4|/dev/sda2:105MB
  #   /dev/sdb:250GB|/dev/sdb1:524MB:ntfs|/dev/sdb2:20.0GB:ntfs|/dev/sdb4:228GB:|/dev/sdb3:1049MB:ntfs
  #
  # TODO: the above knowledge could be encapsulated as test mock data
  #       * make GetDisksPartsData() store into a cache instead of returning results
  #       * call GetDisksPartsData() only once
  #       * replace GetDisksPartsData() in callers with the cache
  #       * write a test suite against these partitioning helpers,
  #         mocking the GetDisksPartsData() cache with the data above
  #       * capture the 7th field (partition type)
  #       * allow automatically format un-formatted swap partitions in Partition()
  #
  # TODO: filter out mounted filesystems

  UnmountAll

  parted --list --machine 2> /dev/null | tr -d '\r' | \
  while read line
  do    if   [[ "$line" =~ ^BYT\;\ *$ ]]                              # device delimiter
        then echo ' '

        elif [[ "${line}" =~ ^(/dev/sd[a-z]):([^:]*): ]]              # device specifier
        then dev="${BASH_REMATCH[1]}"
                 size="${BASH_REMATCH[2]}"
             echo -n "${dev}${DISK_FIELD_SEP}${size}"

        elif [[ "${line}" =~ ^([0-9]):[^:]*:[^:]*:([^:]*):([^:]*): ]] # partition specifier
        then part_n=${BASH_REMATCH[1]}
             size=${BASH_REMATCH[2]}
             format=$( [[ -n "${BASH_REMATCH[3]}" ]] && echo ${BASH_REMATCH[3]} || : )
             echo -n "${DISK_RECORD_SEP}${dev}${part_n}"
             echo -n "${DISK_FIELD_SEP}${size}"
             echo -n "${DISK_FIELD_SEP}${format}"
        fi
  done
}

GetDiskData() # (disk_data_n)
{
  # example GetDiskData() output:
  #   /dev/sda 42.9GB

  if   [[ "$1" =~ ^[0-9]+$ ]]
  then local disk_data_n=$1
       local disks_parts_data=( $(GetDisksPartsData) )
       local disk_data=${disks_parts_data[${disk_data_n}]%%${DISK_RECORD_SEP}*}

       tr "${DISK_FIELD_SEP}" ' ' <<<${disk_data}
  fi
}

GetDiskPartsData() # (disk_data_n)
{
  # example GetDiskPartsData() output:
  #   /dev/sdb1:968MB:ext4 /dev/sdb2:105MB:linux-swap(v1)

  local disk_data_n=$1
  local disks_parts_data=( $(GetDisksPartsData) )
  local disk_parts_data=${disks_parts_data[${disk_data_n}]#*${DISK_RECORD_SEP}}

  tr "${DISK_RECORD_SEP}" ' ' <<<${disk_parts_data}
}

GetDiskPartData() # (disk_data_n part_data_n)
{
  # example GetDiskPartData() output:
  #   /dev/sdb1 968MB ext4

  local disk_data_n=$1
  local part_data_n=$2
  local disk_parts_data=( $(GetDiskPartsData ${disk_data_n}) )
  local disk_part_data=${disk_parts_data[${part_data_n}]}

  tr "${DISK_FIELD_SEP}" ' ' <<<${disk_part_data}
}

GetDevice() # (disk_data_n)
{
  # example GetDevice() output:
  #   /dev/sda

  local disk_data_n=$1
  local disk_data=$(GetDiskData ${disk_data_n})
  local device=${disk_data/ *}

  echo ${device}
}

PopulatePartsData() # (disk_data_n) # sets $PartsData
{
  local disk_data_n=$(( $1 - 1 )) # dialog device_n is 1-based
  local disk_parts_data=( $(GetDiskPartsData ${disk_data_n}) )
  local part_data_n
  local disk_part_data
  local dlg_part_n
  PartsData=()

  # populate storage array for partition data - example PartsData:
  #   [1]="/dev/sdb1 9662MB ext4" [2]="/dev/sdb2 1048kB"
  for (( part_data_n=0 ; part_data_n < ${#disk_parts_data[@]} ; ++part_data_n ))
  do  disk_part_data="$(GetDiskPartData ${disk_data_n} ${part_data_n})"
      dlg_part_n=$(( ${part_data_n} + 1 )) # sparse array for selective deletion

      if ! [[ "${disk_part_data}" =~ linux-swap ]]           && # ignore swap
         ! mount | grep "^${disk_part_data/ *} " > /dev/null    # ignore mounted
      then PartsData[${dlg_part_n}]="${disk_part_data}"
      fi
  done
}

PopulateDisksOptions() # sets $DlgParams
{
  local disks_part_data=( $(GetDisksPartsData) )
  local disk_data_n
  DlgParams=()

  # populate params array for dialog - example DlgParams:
  #   ( 1 "/dev/sda 42.9GB" 2 "/dev/sdb 9664MB" )
  for (( disk_data_n=0 ; disk_data_n < ${#disks_part_data[@]} ; ++disk_data_n ))
  do  DlgParams+=( $(( ${disk_data_n} + 1 )) "$(GetDiskData ${disk_data_n})" )
  done
}

PopulatePartOptions() # sets $DlgParams, assumes $PartsData
{
  local part_data_n
  DlgParams=()

  # populate params array for dialog - example DlgParams:
  #   ( 1 "/dev/sdb1 9662MB ext4" 2 "/dev/sdb2 1048kB" )
  for part_data_n in $(tr ' ' '\n' <<<${!PartsData[@]} | sort)
  do  DlgParams+=( ${part_data_n} "${PartsData[${part_data_n}]}" )
  done
}

RemovePartOption() # (part_n) # modifies $PartsData
{
  local dlg_part_n=$1

  unset 'PartsData['"${dlg_part_n}"']'
  PopulatePartOptions
}


## error logging helpers ##

LogError() # (source_file func_name line_n)
{
  local SOURCE_FILE="${BASH_SOURCE[1]}"
  local FUNC_NAME="$( [[ -n "$1" ]] && echo "$1" || echo "FUNC_NAME" )"
  local LINE_N=$2
  local N_CONTEXT_LINES=3
  local N_LINES=$((      1 + (2 * N_CONTEXT_LINES) ))
  local BEGIN_LINE_N=$(( LINE_N - N_CONTEXT_LINES  ))
  local END_LINE_N=$((   LINE_N + N_CONTEXT_LINES  ))
  local marker line

  (( BEGIN_LINE_N < 0 )) && BEGIN_LINE_N=0

  echo "ERROR: in ${SOURCE_FILE} ${FUNC_NAME}()::${LINE_N}" >&2
  sed 's|\\$||' "${SOURCE_FILE}" | pr -tn                      | \
  tail -n +${BEGIN_LINE_N} | head -n ${N_LINES} | tr '\n' '\n' | \
  while read line
  do    line_n=$(sed -E 's|([0-9]+).*|\1|' <<<${line})
        (( line_n == LINE_N )) && marker='==>' || marker='   '
        printf "%s %s\n" "${marker}" "${line}" >&2
  done
}


## debugging helpers ##

MOCK_INIT()              { SetStateVar 'XKBMAP' ${DEF_KEYMAP} ; SetStateVar 'LANG' ${DEF_LANG} ; SetStateVar 'TR_KEY' ${DEF_TRKEY} ; SetStateVar 'INSTALL' ${DEF_INSTALL} ; }
MOCK_SELECTDEFAULTS()    { SetStateVar 'INSTALL' ${DEF_INSTALL} ; SetStateVar 'BASE' ${DEF_PKG_SET} ; SetStateVar 'INIT' ${DEF_INIT} ; SetStateVar 'WMDE' ${DEF_WMDE} ; SetStateVar 'HOSTNAME' ${DEF_HOSTNAME} ; SetStateVar 'TIMEZONE' ${DEF_TIMEZONE} ; SetStateVar 'KEYMAP' $(GetStateVar 'XKBMAP') ; SetStateVar 'LOCALES' $(GetStateVar 'LANG') ; }
MOCK_SELECTDEFAULTS_NO() { : ; }
MOCK_SELECTLOGINS()      { SetStateVar 'ROOT_PASS' ROOT_PASS ; MOCK_SELECTLOGINS_USER ; }
MOCK_SELECTLOGINS_USER() { SetStateVar 'USER_LOGIN' USER_LOGIN ; SetStateVar 'USER_PASS' USER_PASS ; }
MOCK_SELECTBASE()        { SetStateVar 'INSTALL' ${DEF_INSTALL} ; SetStateVar 'BASE' ${DEF_PKG_SET} ; SetStateVar 'INIT' ${DEF_INIT} ; }
MOCK_SELECTBOOT()        { SetStateVar 'BOOT' 'grub' ; }
MOCK_SELECTWMDE()        { [[ -n "$(GetStateVar 'WMDE')" ]] || SetStateVar 'WMDE' 'cli' ; }
MOCK_SELECTENV()         { SetStateVar 'HOSTNAME' ${DEF_HOSTNAME} ; SetStateVar 'TIMEZONE' ${DEF_TIMEZONE} ; SetStateVar 'KEYMAP' $(GetStateVar 'XKBMAP') ; SetStateVar 'LOCALES' $(GetStateVar 'LANG') ; }


## translations for user-facing strings ##

readonly SWAP_SED_RX='/(\[part_auto-[a-z][a-z]\]=".*) [^ ]+ ([^ ]+\)")/{s//\1 '$((SWAP_MB / 1000))'GB \2/ ; h} ; ${x ; /./{x ; q0} ; x ; q1} ;'
readonly SWAP_SED_ERR_MSG="SWAP_SED_RX failed on translations.sh.inc"
readonly TRANSLATIONS_ERR_MSG="failed to source translations.sh.inc"

if   (( ! IN_CHROOT ))
then # inject size of automatic swap partition into translations, then load the translations
     sed -i -E "${SWAP_SED_RX}" ./translations.sh.inc || ! echo "${SWAP_SED_ERR_MSG}"     || exit 1
     source ./translations.sh.inc                     || ! echo "${TRANSLATIONS_ERR_MSG}" || exit 1
fi


# helper for install.sh
SetLang() { Lang=$( (( ${TRANSLATIONS[$1]} )) && echo $1 || echo ${DEF_TRKEY} ) ; }


# set initial installer language
Lang='' ; SetLang $(GetStateVar 'TR_KEY' ${DEF_TRKEY}) ;