001package ball.spring.mysqld;
002/*-
003 * ##########################################################################
004 * Reusable Spring Components
005 * $Id: MysqldConfiguration.java 7215 2021-01-03 18:39:51Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-spring/trunk/jar/src/main/java/ball/spring/mysqld/MysqldConfiguration.java $
007 * %%
008 * Copyright (C) 2018 - 2021 Allen D. Ball
009 * %%
010 * Licensed under the Apache License, Version 2.0 (the "License");
011 * you may not use this file except in compliance with the License.
012 * You may obtain a copy of the License at
013 *
014 *      http://www.apache.org/licenses/LICENSE-2.0
015 *
016 * Unless required by applicable law or agreed to in writing, software
017 * distributed under the License is distributed on an "AS IS" BASIS,
018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019 * See the License for the specific language governing permissions and
020 * limitations under the License.
021 * ##########################################################################
022 */
023import java.io.File;
024import java.io.IOException;
025import java.lang.ProcessBuilder.Redirect;
026import java.nio.file.Files;
027import javax.annotation.PostConstruct;
028import javax.annotation.PreDestroy;
029import lombok.NoArgsConstructor;
030import lombok.ToString;
031import lombok.extern.log4j.Log4j2;
032import org.springframework.beans.factory.annotation.Value;
033import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
034import org.springframework.context.annotation.Bean;
035import org.springframework.context.annotation.Configuration;
036import org.springframework.scheduling.annotation.EnableScheduling;
037
038import static java.util.concurrent.TimeUnit.SECONDS;
039import static org.apache.commons.lang3.StringUtils.EMPTY;
040
041/**
042 * {@code mysqld} {@link Configuration}.  A {@code mysqld} process is
043 * started if the {@code mysqld.home} application property is set.  In
044 * addition, a port must be specified with the {@code mysqld.port} property.
045 *
046 * {@injected.fields}
047 *
048 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
049 * @version $Revision: 7215 $
050 */
051@Configuration
052@EnableScheduling
053@ConditionalOnProperty(name = "mysqld.home", havingValue = EMPTY)
054@NoArgsConstructor @ToString @Log4j2
055public class MysqldConfiguration {
056    @Value("${mysqld.home}")
057    private File home;
058
059    @Value("${mysqld.defaults.file:${mysqld.home}/my.cnf}")
060    private File defaults;
061
062    @Value("${mysqld.datadir:${mysqld.home}/data}")
063    private File datadir;
064
065    @Value("${mysqld.port}")
066    private Integer port;
067
068    @Value("${mysqld.socket:${mysqld.home}/socket}")
069    private File socket;
070
071    @Value("${logging.file.path}/mysqld.log")
072    private File console;
073
074    private volatile Process mysqld = null;
075
076    @PostConstruct
077    public void init() { }
078
079    @Bean
080    public Process mysqld() throws IOException {
081        if (mysqld == null) {
082            synchronized (this) {
083                if (mysqld == null) {
084                    Files.createDirectories(home.toPath());
085                    Files.createDirectories(datadir.toPath().getParent());
086                    Files.createDirectories(console.toPath().getParent());
087
088                    String defaultsArg = "--no-defaults";
089
090                    if (defaults.exists()) {
091                        defaultsArg =
092                            "--defaults-file=" + defaults.getAbsolutePath();
093                    }
094
095                    String datadirArg = "--datadir=" + datadir.getAbsolutePath();
096                    String socketArg = "--socket=" + socket.getAbsolutePath();
097                    String portArg = "--port=" + port;
098
099                    if (! datadir.exists()) {
100                        try {
101                            new ProcessBuilder("mysqld",
102                                               defaultsArg, datadirArg,
103                                               "--initialize-insecure")
104                                .directory(home)
105                                .inheritIO()
106                                .redirectOutput(Redirect.to(console))
107                                .redirectErrorStream(true)
108                                .start()
109                                .waitFor();
110                        } catch (InterruptedException exception) {
111                        }
112                    }
113
114                    if (datadir.exists()) {
115                        socket.delete();
116
117                        mysqld =
118                            new ProcessBuilder("mysqld",
119                                               defaultsArg, datadirArg,
120                                               socketArg, portArg)
121                            .directory(home)
122                            .inheritIO()
123                            .redirectOutput(Redirect.appendTo(console))
124                            .redirectErrorStream(true)
125                            .start();
126
127                        while (! socket.exists()) {
128                            try {
129                                mysqld.waitFor(15, SECONDS);
130                            } catch (InterruptedException exception) {
131                            }
132
133                            if (mysqld.isAlive()) {
134                                continue;
135                            } else {
136                                throw new IllegalStateException("mysqld not started");
137                            }
138                        }
139                    } else {
140                        throw new IllegalStateException("mysqld datadir does not exist");
141                    }
142                }
143            }
144        }
145
146        return mysqld;
147    }
148
149    @PreDestroy
150    public void destroy() {
151        if (mysqld != null) {
152            try {
153                for (int i = 0; i < 8; i+= 1) {
154                    if (mysqld.isAlive()) {
155                        mysqld.destroy();
156                        mysqld.waitFor(15, SECONDS);
157                    } else {
158                        break;
159                    }
160                }
161            } catch (InterruptedException exception) {
162            }
163
164            try {
165                if (mysqld.isAlive()) {
166                    mysqld.destroyForcibly().waitFor(60, SECONDS);
167                }
168            } catch (InterruptedException exception) {
169            }
170        }
171    }
172}