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