001package ball.spring.mysqld;
002/*-
003 * ##########################################################################
004 * Reusable Spring Components
005 * $Id: MysqldConfiguration.html 5431 2020-02-12 19:03:17Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/hcf-dev/blog/2019-10-19-spring-embedded-mysqld/src/main/resources/javadoc/src-html/ball/spring/mysqld/MysqldConfiguration.html $
007 * %%
008 * Copyright (C) 2018 - 2020 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;
036
037import static java.util.concurrent.TimeUnit.SECONDS;
038import static org.apache.commons.lang3.StringUtils.EMPTY;
039
040/**
041 * {@code mysqld} {@link Configuration}.  A {@code mysqld} process is
042 * started if the {@code mysqld.home} application property is set.  In
043 * addition, a port must be specified with the {@code mysqld.port} property.
044 *
045 * {@injected.fields}
046 *
047 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
048 * @version $Revision: 5431 $
049 */
050@Configuration
051@ConditionalOnProperty(name = "mysqld.home", havingValue = EMPTY)
052@NoArgsConstructor @ToString @Log4j2
053public class MysqldConfiguration {
054    @Value("${mysqld.home}")
055    private File home;
056
057    @Value("${mysqld.defaults.file:${mysqld.home}/my.cnf}")
058    private File defaults;
059
060    @Value("${mysqld.datadir:${mysqld.home}/data}")
061    private File datadir;
062
063    @Value("${mysqld.port}")
064    private Integer port;
065
066    @Value("${mysqld.socket:${mysqld.home}/socket}")
067    private File socket;
068
069    @Value("${logging.path}/mysqld.log")
070    private File console;
071
072    private volatile Process mysqld = null;
073
074    @PostConstruct
075    public void init() { }
076
077    @Bean
078    public Process mysqld() throws IOException {
079        if (mysqld == null) {
080            synchronized (this) {
081                if (mysqld == null) {
082                    Files.createDirectories(home.toPath());
083                    Files.createDirectories(datadir.toPath().getParent());
084                    Files.createDirectories(console.toPath().getParent());
085
086                    String defaultsArg = "--no-defaults";
087
088                    if (defaults.exists()) {
089                        defaultsArg =
090                            "--defaults-file=" + defaults.getAbsolutePath();
091                    }
092
093                    String datadirArg = "--datadir=" + datadir.getAbsolutePath();
094                    String socketArg = "--socket=" + socket.getAbsolutePath();
095                    String portArg = "--port=" + port;
096
097                    if (! datadir.exists()) {
098                        try {
099                            new ProcessBuilder("mysqld",
100                                               defaultsArg, datadirArg,
101                                               "--initialize-insecure")
102                                .directory(home)
103                                .inheritIO()
104                                .redirectOutput(Redirect.to(console))
105                                .redirectErrorStream(true)
106                                .start()
107                                .waitFor();
108                        } catch (InterruptedException exception) {
109                        }
110                    }
111
112                    if (datadir.exists()) {
113                        socket.delete();
114
115                        mysqld =
116                            new ProcessBuilder("mysqld",
117                                               defaultsArg, datadirArg,
118                                               socketArg, portArg)
119                            .directory(home)
120                            .inheritIO()
121                            .redirectOutput(Redirect.appendTo(console))
122                            .redirectErrorStream(true)
123                            .start();
124
125                        while (! socket.exists()) {
126                            try {
127                                mysqld.waitFor(15, SECONDS);
128                            } catch (InterruptedException exception) {
129                            }
130
131                            if (mysqld.isAlive()) {
132                                continue;
133                            } else {
134                                throw new IllegalStateException("mysqld not started");
135                            }
136                        }
137                    } else {
138                        throw new IllegalStateException("mysqld datadir does not exist");
139                    }
140                }
141            }
142        }
143
144        return mysqld;
145    }
146
147    @PreDestroy
148    public void destroy() {
149        if (mysqld != null) {
150            try {
151                for (int i = 0; i < 8; i+= 1) {
152                    if (mysqld.isAlive()) {
153                        mysqld.destroy();
154                        mysqld.waitFor(15, SECONDS);
155                    } else {
156                        break;
157                    }
158                }
159            } catch (InterruptedException exception) {
160            }
161
162            try {
163                if (mysqld.isAlive()) {
164                    mysqld.destroyForcibly().waitFor(60, SECONDS);
165                }
166            } catch (InterruptedException exception) {
167            }
168        }
169    }
170}