4 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs]"
6 echo " start: Start/initialize a container"
7 echo " stop: Stop a running container"
8 echo " restart: Restart a container"
9 echo " destroy: Stop and remove a container"
10 echo " enter: Use nsenter to enter a container"
11 echo " logs: Docker logs for container"
12 echo " bootstrap: Bootstrap a container for the config based on a template"
13 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
14 echo " cleanup: Remove all containers that have stopped for > 24 hours"
17 echo " --skip-prereqs Don't check prerequisites or resource requirements"
18 echo " --docker-args Extra arguments to pass when running docker"
26 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
27 re
='[A-Z/ !@#$%^&*()+~`=]'
28 if [[ $config =~
$re ]];
31 echo "ERROR: Config name must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
38 docker_min_version
='1.6.0'
39 docker_rec_version
='1.6.0'
41 config_file
=containers
/"$config".yml
42 cidbootstrap
=cids
/"$config"_bootstrap.cid
43 local_discourse
=local_discourse
44 image
=discourse
/discourse
:1.0.17
45 docker_path
=`which docker.io || which docker`
46 template_path
=samples
/standalone.yml
47 changelog
=/tmp
/changelog
# used to test whether sed did anything
49 if [ "${SUPERVISED}" = "true" ]; then
50 restart_policy
="--restart=no"
52 attach_on_run
="-a stdout -a stderr"
57 if [ -n "$DOCKER_HOST" ]; then
58 docker_ip
=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
59 elif [ -x "$(which ip 2>/dev/null)" ]; then
60 docker_ip
=`ip addr show docker0 | \
62 awk '{ split($2,a,"/"); print a[1] }';`
64 docker_ip
=`ifconfig | \
65 grep -B1 "inet addr" | \
66 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
68 awk -F: '{ print $3 }';`
74 IFS
=.
read -a ver_a
<<< "$1"
75 IFS
=.
read -a ver_b
<<< "$2"
77 while [[ -n $ver_a ]]; do
78 if (( ver_a
> ver_b
)); then
80 elif (( ver_b
> ver_a
)); then
89 return 1 # They are equal
95 echo "Docker is not installed, you will need to install Docker in order to run Discourse"
96 echo "Please visit https://docs.docker.com/installation/ for instructions on how to do this for your system"
98 echo "If you are running a recent Ubuntu Server, try the following:"
99 echo "sudo apt-get install docker-engine"
106 if [ -z $docker_path ]; then
110 # 1. docker daemon running?
111 # we send stderr to /dev/null cause we don't care about warnings,
112 # it usually complains about swap which does not matter
113 test=`$docker_path info 2> /dev/null`
114 if [[ $?
-ne 0 ]] ; then
115 echo "Cannot connect to the docker daemon - verify it is running and you have access"
119 # 2. running aufs or btrfs
120 test=`$docker_path info 2> /dev/null | grep 'Driver: '`
121 if [[ "$test" =~
[aufs|btrfs|zfs|overlay
] ]] ; then : ; else
122 echo "Your Docker installation is not using a supported filesystem if we were to proceed you may have a broken install."
123 echo "aufs is the recommended filesystem you should be using (zfs/btrfs and overlay may work as well)"
124 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the driver"
126 echo "If you wish to continue anyway using your existing unsupported filesystem, "
127 echo "read the source code of launcher and figure out how to bypass this."
131 # 3. running recommended docker version
132 test=($
($docker_path --version)) # Get docker version string
133 test=${test[2]//,/} # Get version alone and strip comma if exists
135 # At least minimum version
136 if compare_version
"${docker_min_version}" "${test}"; then
137 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
141 # Recommend best version
142 if compare_version
"${docker_rec_version}" "${test}"; then
143 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
146 # 4. discourse docker image is downloaded
147 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
149 if [ -z "$test" ]; then
151 echo "WARNING: We are about to start downloading the Discourse base image"
152 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
154 echo "Please be patient"
159 # 5. able to attach stderr / out / tty
160 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
161 if [[ "$test" =~
"working" ]] ; then : ; else
162 echo "Your Docker installation is not working correctly"
164 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
173 avail_mem
="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
174 if [ "$avail_mem" -lt 900 ]; then
175 resources
="insufficient"
176 echo "WARNING: You do not appear to have sufficient memory to run Discourse."
178 echo "Your system may not work properly, or future upgrades of Discourse may"
179 echo "not complete successfully."
181 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#create-new-cloud-server"
182 elif [ "$avail_mem" -lt 1800 ]; then
183 total_swap
="$(LANG=C free -m | grep ^Swap: | awk '{print $2}')"
184 if [ "$total_swap" -lt 1000 ]; then
185 resources
="insufficient"
186 echo "WARNING: You must have at least 1GB of swap when running with less"
187 echo "than 2GB of RAM."
189 echo "Your system may not work properly, or future upgrades of Discourse may"
190 echo "not complete successfully."
192 echo "See https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#set-up-swap-if-needed"
197 free_disk
="$(df /var | tail -n 1 | awk '{print $4}')"
198 if [ "$free_disk" -lt 5000 ]; then
199 resources
="insufficient"
200 echo "WARNING: You must have at least 5GB of *free* disk space to run Discourse."
202 echo "Insufficient disk space may result in problems running your site, and may"
203 echo "not even allow Discourse installation to complete successfully."
205 echo "Please free up some space, or expand your disk, before continuing."
207 echo "Run \`apt-get autoremove && apt-get autoclean\` to clean up unused packages and \`./launcher cleanup\` to remove stale Docker containers."
211 if [ -t 0 ] && [ "$resources" != "ok" ]; then
213 read -p "Press ENTER to continue, or Ctrl-C to exit and give your system more resources"
218 local valid
=$
(netstat
-tln |
awk '{print $4}' |
grep ":${1}\$")
220 if [ -n "$valid" ]; then
221 echo "Launcher has detected that port ${1} is in use."
223 echo "If you are trying to run Discourse simultaneously with another web server like Apache or nginx, you will need to bind to a different port."
224 echo "See https://meta.discourse.org/t/17247 for help."
225 echo "To continue anyway, re-run Launcher with --skip-prereqs"
230 if [ "$opt" != "--skip-prereqs" ] ; then
234 if [ "$opt" == "--docker-args" ] ; then
241 read -r -d '' env_ruby
<< 'RUBY'
244 input
= STDIN.readlines.
join
245 yaml
= YAML.load
(input
)
247 if host_run
= yaml
['host_run']
248 params
= yaml
['params'] ||
{}
249 host_run.each
do |run|
251 run
= run.gsub
("$#{k}", v
)
253 STDOUT.
write "#{run}--SEP--"
258 host_run
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
260 while [ "$host_run" ] ; do
261 iter
=${host_run%%--SEP--*}
263 echo "Host run: $iter"
266 host_run
="${host_run#*--SEP--}"
272 volumes
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
273 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
277 links
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
278 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
281 set_template_info
() {
283 templates
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
284 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
286 arrTemplates
=(${templates// / })
287 config_data
=$
(cat $config_file)
291 for template
in "${arrTemplates[@]}"
293 [ ! -z $template ] && {
294 input
="$input _FILE_SEPERATOR_ $(cat $template)"
298 # we always want our config file last so it takes priority
299 input
="$input _FILE_SEPERATOR_ $config_data"
301 read -r -d '' env_ruby
<< 'RUBY'
304 input
=STDIN.readlines.
join
305 # default to UTF-8 for the dbs sake
306 env
= {'LANG' => 'en_US.UTF-8'}
307 input.
split('_FILE_SEPERATOR_').each
do |yml|
310 env.merge
!(YAML.load
(yml
)['env'] ||
{})
311 rescue Psych
::SyntaxError
=> e
319 puts env.map
{|k
,v|
"-e\n#{k}=#{v}" }.
join("\n")
322 raw
=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
327 if [ "$i" == "*ERROR." ]; then
329 elif [ -n "$i" ]; then
334 if [ "$ok" -ne 1 ]; then
336 echo "YAML syntax error. Please check your /var/discourse/containers/*.yml config files."
341 if [ -z $docker_path ]; then
345 [ "$command" == "cleanup" ] && {
347 echo "The following command will"
348 echo "- Delete all docker images for old containers"
349 echo "- Delete all stopped and orphan containers"
351 read -p "Are you sure (Y/n): " -n 1 -r && echo
352 if [[ $REPLY =~ ^
[Yy
]$ ||
! $REPLY ]]
354 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
355 echo "Starting Cleanup (bytes free $space)"
357 STATE_DIR
=.
/.gc-state
scripts
/docker-gc
359 space
=$
(df
/var
/lib
/docker |
awk '{ print $4 }' |
grep -v Available
)
360 echo "Finished Cleanup (bytes free $space)"
372 if [[ ! -e $config_file && $command -ne "memconfig" ]]
374 echo "Config file was not found, ensure $config_file exists"
376 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
380 docker_version
=($
($docker_path --version))
381 docker_version
=${test[2]//,/}
382 restart_policy
=${restart_policy:---restart=always}
384 set_existing_container
(){
385 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
390 set_existing_container
392 if [ ! -z $existing ]
396 $docker_path stop
-t 10 $config
399 echo "$config was not started !"
405 run_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
406 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
408 if [ -z "$run_image" ]; then
409 run_image
="$local_discourse/$config"
414 boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
415 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
417 if [ -z "$boot_command" ]; then
419 no_boot_command
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
420 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
422 if [ -z "$no_boot_command" ]; then
423 boot_command
="/sbin/boot"
428 scale_ram_and_cpu
() {
430 # grab info about total system ram and physical (NOT LOGICAL!) CPU cores
431 avail_mem
="$(LANG=C free -m | grep '^Mem:' | awk '{print $2}')"
432 avail_gb
=$
(( $avail_mem / 950 ))
433 avail_cores
=`cat /proc/cpuinfo | grep "cpu cores" | uniq | awk '{print $4}'`
434 echo "Found ${avail_gb}GB of memory and $avail_cores physical CPU cores"
436 # db_shared_buffers: 128MB for 1GB, 256MB for 2GB, or 256MB * GB, max 4096MB
437 if [ "$avail_gb" -eq "1" ]
439 db_shared_buffers
=128
441 if [ "$avail_gb" -eq "2" ]
443 db_shared_buffers
=256
445 db_shared_buffers
=$
(( 256 * $avail_gb ))
448 db_shared_buffers
=$
(( db_shared_buffers
< 4096 ? db_shared_buffers
: 4096 ))
450 sed -i -e "s/^ #db_shared_buffers:.*/ db_shared_buffers: \"${db_shared_buffers}MB\"/w $changelog" $config_file
453 echo "setting db_shared_buffers = ${db_shared_buffers}MB based on detected CPU/RAM"
458 # UNICORN_WORKERS: 2 * GB for 2GB or less, or 2 * CPU, max 8
459 if [ "$avail_gb" -le "2" ]
461 unicorn_workers
=$
(( 2 * $avail_gb ))
463 unicorn_workers
=$
(( 2 * $avail_cores ))
465 unicorn_workers
=$
(( unicorn_workers
< 8 ? unicorn_workers
: 8 ))
467 sed -i -e "s/^ #UNICORN_WORKERS:.*/ UNICORN_WORKERS: ${unicorn_workers}/w $changelog" $config_file
470 echo "setting UNICORN_WORKERS = ${unicorn_workers} based on detected CPU/RAM"
478 if [ -f ~
/tmp
/debugjp
]
480 echo "THIS CODE SHOULD NEVER BE COMMITTED!!"
482 if grep -q "DISCOURSE_HOSTNAME: 'discourse.example.com'" $config_file
484 local hostname
="discourse.example.com"
486 echo "DISCOURSE_HOSTNAME set. Not changing."
488 if grep -q "DISCOURSE_DEVELOPER_EMAILS: 'me@example.com'" $config_file
490 local developer_emails
="me@example.com"
492 echo "DISCOURSE_DEVELOPER_EMAILS set. Not changing."
494 if grep -q "DISCOURSE_SMTP_ADDRESS: smtp.example.com" $config_file
496 local smtp_address
="smtp.example.com"
498 echo "DISCOURSE_SMTP_ADDRESS set. Not changing."
500 if grep -q "#DISCOURSE_SMTP_USER_NAME: user@example.com" $config_file
502 local smtp_user_name
="user@example.com"
504 echo "SMTP_USER_NAME set. Not changing."
506 if grep -q "#DISCOURSE_SMTP_PASSWORD: pa\$\$word" $config_file
508 local smtp_password
="pa\$\$word"
510 echo "DISCOURSE_SMTP_PASSWORD set. Not changing."
512 if grep -q "#LETSENCRYPT_ACCOUNT_EMAIL:" $config_file
514 local letsencrypt_account_email
="your.email@example.com"
516 echo "#LETSENCRYPT_ACCOUNT_EMAIL set. Not changing."
518 #- "templates/web.ssl.template.yml"
519 #- "templates/web.letsencrypt.ssl.template.yml"
520 #DISCOURSE_CDN_URL: //discourse-cdn.example.com
524 local letsencrypt_status
="change to enable"
525 while [ $config_ok == "n" ]
527 echo "Getting config"
528 if [ ! -z $hostname ]
530 read -p "hostname: [$hostname]: " new_value
531 if [ ! -z $new_value ]
538 if [ ! -z $developer_emails ]
540 read -p "developer_emails [$developer_emails]: " new_value
541 if [ ! -z $new_value ]
543 developer_emails
=$new_value
546 if [ ! -z $smtp_address ]
548 read -p "smtp_address [$smtp_address]: " new_value
549 if [ ! -z $new_value ]
551 smtp_address
=$new_value
554 if [ ! -z $smtp_user_name ]
556 read -p "smtp_user_name [$smtp_user_name]: " new_value
557 if [ ! -z $new_value ]
559 smtp_user_name
=$new_value
562 if [ ! -z $smtp_password ]
564 read -p "smtp_password [$smtp_password]: " new_value
565 if [ ! -z $new_value ]
567 smtp_password
=$new_value
570 if [ ! -z $letsencrypt_account_email ]
572 read -p "letsencrypt_account_email ($letsencrypt_status) [$letsencrypt_account_email]: " new_value
573 if [ ! -z $new_value ]
575 letsencrypt_account_emails
=$new_value
576 letsencrypt_status
="enabled"
577 echo "Letsencrypt enabled."
579 echo "letsencrypt unchanged"
583 echo -e "\n\nThat's it! Everything is set. How does it look?\n"
585 echo "DISCOURSE_HOSTNAME: $hostname"
586 echo "DISCOURSE_DEVELOPER_EMAILS: $developer_emails"
587 echo "DISCOURSE_SMTP_ADDRESS: $smtp_address"
588 echo "DISCOURSE_SMTP_USER_NAME: $smtp_user_name"
589 echo "DISCOURSE_SMTP_PASSWORD: $smtp_password"
590 echo -e "DISCOURSE_SMTP_ENABLE_START_TLS: $smtp_enable_start_tls\c"
591 if [ $letsencrypt_status == "enabled" ]
593 echo "LETSENCRYPT_ACCOUNT_EMAIL: $letsencrypt_account_emails"
594 echo "LETSENCRYPT enabled."
596 echo "LETSENCRYPT not enabled."
599 read -p "Enter to continue with these settings, 'N' to edit or ^C to start again:" config_ok
602 echo "CONFIG DONE! BUT NOT WRITTEN"
603 echo "THIS CODE SHOULD NEVER BE COMMITTED!!"
605 if [ -f ~
/tmp
/debugjp
]
607 echo "IF you are reading this you should delete this line and the next one."
614 existing
=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
616 if [ ! -z $existing ]
618 echo "Nothing to do, your container has already started!"
622 existing
=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
623 if [ ! -z $existing ]
625 echo "starting up existing container"
628 $docker_path start
$config
634 ports
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
635 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
637 IFS
='-p ' read -a array
<<< "$ports"
638 for element
in "${array[@]}"
640 IFS
=':' read -a args
<<< "$element"
641 if [ "${#args[@]}" == "2" ]; then
642 check_ports
"${args[0]}"
643 elif [ "${#args[@]}" == "3" ]; then
644 check_ports
"${args[1]}"
648 docker_args
=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
649 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
657 # get hostname and settings from container configuration
658 for envar
in "${env[@]}"
660 if [[ $envar == DOCKER_USE_HOSTNAME
* ]] ||
[[ $envar == DISCOURSE_HOSTNAME
* ]]
662 # use as environment variable
668 hostname
=`hostname -s`
670 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
672 hostname
=$DISCOURSE_HOSTNAME
674 hostname
=$hostname-$config
677 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
678 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
679 # docker added more hostname rules
680 hostname
=${hostname/_/-}
683 $docker_path run
$user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname" \
684 -e DOCKER_HOST_IP
=$docker_ip --name $config -t $ports $volumes $docker_args $run_image $boot_command
691 valid_config_check
() {
694 for x
in DISCOURSE_SMTP_ADDRESS DISCOURSE_SMTP_USER_NAME DISCOURSE_SMTP_PASSWORD \
695 DISCOURSE_DEVELOPER_EMAILS DISCOURSE_HOSTNAME
697 mail_var
=`grep "^ $x:" $config_file`
699 local default
="example.com"
702 if [[ $mail_var = *"$default"* ]]
704 echo "Warning: $x left at incorrect default of example.com"
708 echo "Warning: $x not configured"
712 if [ -t 0 ] && [ "$valid_config" != "y" ]; then
714 read -p "Press Ctrl-C to exit and edit $config_file or ENTER to continue"
720 # Does your system meet the minimum requirements?
721 if [ "$opt" != "--skip-prereqs" ] ; then
728 # is our configuration file valid?
731 # make minor scaling adjustments for RAM and CPU
734 # I got no frigging clue what this does, ask Sam Saffron. It RUNS STUFF ON THE HOST I GUESS?
737 # Is the image available?
738 # If not, pull it here so the user is aware what's happening.
739 $docker_path history $image >/dev
/null
2>&1 ||
$docker_path pull
$image
743 base_image
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
744 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
746 update_pups
=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
747 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
749 if [[ ! X
"" = X
"$base_image" ]]; then
758 run_command
="cd /pups &&"
759 if [[ ! "false" = $update_pups ]]; then
760 run_command
="$run_command git pull &&"
762 run_command
="$run_command /pups/bin/pups --stdin"
766 (exec echo "$input" |
$docker_path run
$user_args $links "${env[@]}" -e DOCKER_HOST_IP
=$docker_ip --cidfile $cidbootstrap -i -a stdin
-a stdout
-a stderr
$volumes $image \
767 /bin
/bash
-c "$run_command") \
768 ||
($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
770 [ ! -e $cidbootstrap ] && echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one" && exit 1
774 $docker_path commit
`cat $cidbootstrap` $local_discourse/$config ||
echo 'FAILED TO COMMIT'
775 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
783 echo "Successfully bootstrapped, to startup use ./launcher start $config"
788 exec $docker_path exec -it $config /bin
/bash
--login
798 $docker_path logs
$config
814 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
815 echo "Ensuring discourse docker is up to date"
819 LOCAL
=$
(git rev-parse @
)
820 REMOTE
=$
(git rev-parse @
{u
})
821 BASE
=$
(git merge-base @ @
{u
})
823 if [ $LOCAL = $REMOTE ]; then
824 echo "Discourse Docker is up-to-date"
826 elif [ $LOCAL = $BASE ]; then
827 echo "Updating Discourse Docker"
828 git pull ||
(echo 'failed to update' && exit 1)
831 elif [ $REMOTE = $BASE ]; then
832 echo "Your version of Discourse Docker is ahead of origin"
835 echo "Discourse Docker has diverged source, this is only expected in Dev mode"
840 set_existing_container
842 if [ ! -z $existing ]
844 echo "Stopping old container"
847 $docker_path stop
-t 10 $config
853 if [ ! -z $existing ]
855 echo "Removing old container"
858 $docker_path rm $config
868 (set -x; $docker_path stop
-t 10 $config && $docker_path rm $config) ||
(echo "$config was not found" && exit 0)