2017-04-01 07:37:48 +02:00
package net.ME1312.SubServers.Host.Executable ;
import net.ME1312.SubServers.Host.Library.Config.YAMLSection ;
import net.ME1312.SubServers.Host.Library.Container ;
import net.ME1312.SubServers.Host.Library.Exception.InvalidServerException ;
2017-05-30 21:38:51 +02:00
import net.ME1312.SubServers.Host.Library.Exception.InvalidTemplateException ;
2017-07-23 06:32:57 +02:00
import net.ME1312.SubServers.Host.Library.Exception.SubCreatorException ;
import net.ME1312.SubServers.Host.Library.NamedContainer ;
2017-04-01 07:37:48 +02:00
import net.ME1312.SubServers.Host.Library.UniversalFile ;
import net.ME1312.SubServers.Host.Library.Util ;
import net.ME1312.SubServers.Host.Library.Version.Version ;
import net.ME1312.SubServers.Host.Network.Packet.PacketExCreateServer ;
import net.ME1312.SubServers.Host.Network.Packet.PacketOutExLogMessage ;
2017-04-16 19:02:14 +02:00
import net.ME1312.SubServers.Host.ExHost ;
2017-04-01 07:37:48 +02:00
import org.json.JSONObject ;
import org.w3c.dom.Document ;
import org.w3c.dom.Node ;
import org.w3c.dom.NodeList ;
import org.xml.sax.InputSource ;
import javax.xml.parsers.DocumentBuilderFactory ;
import java.io.* ;
import java.net.URL ;
import java.nio.charset.Charset ;
2017-07-23 06:32:57 +02:00
import java.util.* ;
2017-09-24 05:19:22 +02:00
import java.util.regex.Pattern ;
2017-04-01 07:37:48 +02:00
/ * *
* Internal SubCreator Class
* /
public class SubCreator {
2017-04-16 19:02:14 +02:00
private ExHost host ;
2017-07-23 06:32:57 +02:00
private TreeMap < String , NamedContainer < Thread , NamedContainer < SubLogger , Process > > > thread ;
2017-04-01 07:37:48 +02:00
2017-05-30 21:38:51 +02:00
public static class ServerTemplate {
private String name ;
private String nick = null ;
private boolean enabled ;
private String icon ;
private File directory ;
private ServerType type ;
private YAMLSection build ;
private YAMLSection options ;
/ * *
* Create a SubCreator Template
*
* @param name Template Name
* @param directory Template Directory
* @param build Build Options
* @param options Configuration Options
* /
public ServerTemplate ( String name , boolean enabled , String icon , File directory , YAMLSection build , YAMLSection options ) {
if ( Util . isNull ( name , enabled , directory , build , options ) ) throw new NullPointerException ( ) ;
if ( name . contains ( " " ) ) throw new InvalidTemplateException ( " Template names cannot have spaces: " + name ) ;
this . name = name ;
this . enabled = enabled ;
this . icon = icon ;
this . directory = directory ;
this . type = ( build . contains ( " Server-Type " ) ) ? ServerType . valueOf ( build . getRawString ( " Server-Type " ) . toUpperCase ( ) ) : ServerType . CUSTOM ;
this . build = build ;
this . options = options ;
}
/ * *
* Get the Name of this Template
*
* @return Template Name
* /
public String getName ( ) {
return name ;
}
/ * *
* Get the Display Name of this Template
*
* @return Display Name
* /
public String getDisplayName ( ) {
return ( nick = = null ) ? getName ( ) : nick ;
}
/ * *
* Sets the Display Name for this Template
*
* @param value Value ( or null to reset )
* /
public void setDisplayName ( String value ) {
if ( value = = null | | value . length ( ) = = 0 | | getName ( ) . equals ( value ) ) {
this . nick = null ;
} else {
this . nick = value ;
}
}
/ * *
* Get the Enabled Status of this Template
*
* @return Enabled Status
* /
public boolean isEnabled ( ) {
return enabled ;
}
/ * *
* Set the Enabled Status of this Template
*
* @param value Value
* /
public void setEnabled ( boolean value ) {
enabled = value ;
}
/ * *
* Get the Item Icon for this Template
*
* @return Item Icon Name / ID
* /
public String getIcon ( ) {
return icon ;
}
/ * *
* Set the Item Icon for this Template
*
* @param value Value
* /
public void setIcon ( String value ) {
icon = value ;
}
/ * *
* Get the Directory for this Template
*
* @return Directory
* /
public File getDirectory ( ) {
return directory ;
}
/ * *
* Get the Type of this Template
*
* @return Template Type
* /
public ServerType getType ( ) {
return type ;
}
/ * *
* Get the Build Options for this Template
*
* @return Build Options
* /
public YAMLSection getBuildOptions ( ) {
return build ;
}
/ * *
* Get the Configuration Options for this Template
*
* @return Configuration Options
* /
public YAMLSection getConfigOptions ( ) {
return options ;
}
}
2017-04-01 07:37:48 +02:00
public enum ServerType {
SPIGOT ,
VANILLA ,
2017-05-30 21:38:51 +02:00
SPONGE ,
CUSTOM ;
2017-04-01 07:37:48 +02:00
@Override
public String toString ( ) {
return super . toString ( ) . substring ( 0 , 1 ) . toUpperCase ( ) + super . toString ( ) . substring ( 1 ) . toLowerCase ( ) ;
}
}
/ * *
* Creates a SubCreator Instance
*
* @param host SubServers . Host
* /
2017-04-16 19:02:14 +02:00
public SubCreator ( ExHost host ) {
2017-04-01 07:37:48 +02:00
if ( Util . isNull ( host ) ) throw new NullPointerException ( ) ;
this . host = host ;
2017-07-23 06:32:57 +02:00
this . thread = new TreeMap < > ( ) ;
2017-04-01 07:37:48 +02:00
}
2017-07-23 06:32:57 +02:00
private YAMLSection build ( NamedContainer < SubLogger , Process > thread , File dir , String name , ServerTemplate template , Version version , UUID address , List < ServerTemplate > history ) throws SubCreatorException {
YAMLSection server = new YAMLSection ( ) ;
boolean error = false ;
if ( history . contains ( template ) ) throw new IllegalStateException ( " Template Import loop detected " ) ;
history . add ( template ) ;
for ( String other : template . getBuildOptions ( ) . getStringList ( " Import " , new ArrayList < String > ( ) ) ) {
if ( host . templates . keySet ( ) . contains ( other . toLowerCase ( ) ) ) {
YAMLSection config = build ( thread , dir , other , host . templates . get ( other . toLowerCase ( ) ) , version , address , history ) ;
if ( config = = null ) {
throw new SubCreatorException ( ) ;
} else {
server . setAll ( config ) ;
}
2017-07-25 21:27:40 +02:00
} else {
thread . name ( ) . logger . warn . println ( " Skipping missing template: " + other ) ;
host . subdata . sendPacket ( new PacketOutExLogMessage ( address , " Skipping missing template: " + other ) ) ;
2017-07-23 06:32:57 +02:00
}
}
server . setAll ( template . getConfigOptions ( ) ) ;
2017-05-30 21:38:51 +02:00
try {
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . info . println ( " Loading Template: " + template . getDisplayName ( ) ) ;
host . subdata . sendPacket ( new PacketOutExLogMessage ( address , " Loading Template: " + template . getDisplayName ( ) ) ) ;
2017-05-30 21:38:51 +02:00
Util . copyDirectory ( template . getDirectory ( ) , dir ) ;
2017-07-23 20:21:05 +02:00
if ( template . getType ( ) = = ServerType . VANILLA ) {
2018-04-07 06:59:22 +02:00
String commit ;
if ( version . compareTo ( new Version ( " 1.12 " ) ) > = 0 ) {
commit = " b8f7aa8b3988e164b04911cc47b54b4cc8a554a1 " ;
} else {
commit = " df9797496a7c148aaeec73a13cda521275b12db8 " ;
}
version = new Version ( version . toString ( ) + " " + commit ) ;
2017-07-23 20:21:05 +02:00
} else if ( template . getType ( ) = = ServerType . SPONGE ) {
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . info . println ( " Searching Versions... " ) ;
2017-04-01 07:37:48 +02:00
host . subdata . sendPacket ( new PacketOutExLogMessage ( address , " Searching Versions... " ) ) ;
Document spongexml = DocumentBuilderFactory . newInstance ( ) . newDocumentBuilder ( ) . parse ( new InputSource ( new StringReader ( Util . readAll ( new BufferedReader ( new InputStreamReader ( new URL ( " http://files.minecraftforge.net/maven/org/spongepowered/spongeforge/maven-metadata.xml " ) . openStream ( ) , Charset . forName ( " UTF-8 " ) ) ) ) ) ) ) ;
Document forgexml = DocumentBuilderFactory . newInstance ( ) . newDocumentBuilder ( ) . parse ( new InputSource ( new StringReader ( Util . readAll ( new BufferedReader ( new InputStreamReader ( new URL ( " http://files.minecraftforge.net/maven/net/minecraftforge/forge/maven-metadata.xml " ) . openStream ( ) , Charset . forName ( " UTF-8 " ) ) ) ) ) ) ) ;
NodeList spnodeList = spongexml . getElementsByTagName ( " version " ) ;
Version spversion = null ;
for ( int i = 0 ; i < spnodeList . getLength ( ) ; i + + ) {
Node node = spnodeList . item ( i ) ;
if ( node . getNodeType ( ) = = Node . ELEMENT_NODE ) {
if ( node . getTextContent ( ) . startsWith ( version . toString ( ) + '-' ) & & ( spversion = = null | | new Version ( node . getTextContent ( ) ) . compareTo ( spversion ) > = 0 ) ) {
spversion = new Version ( node . getTextContent ( ) ) ;
}
}
}
2017-05-30 21:38:51 +02:00
if ( spversion = = null )
throw new InvalidServerException ( " Cannot find sponge version for Minecraft " + version . toString ( ) ) ;
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . info . println ( " Found \" spongeforge- " + spversion . toString ( ) + '"' ) ;
2017-04-01 07:37:48 +02:00
host . subdata . sendPacket ( new PacketOutExLogMessage ( address , " Found \" spongeforge- " + spversion . toString ( ) + '"' ) ) ;
NodeList mcfnodeList = forgexml . getElementsByTagName ( " version " ) ;
Version mcfversion = null ;
for ( int i = 0 ; i < mcfnodeList . getLength ( ) ; i + + ) {
Node node = mcfnodeList . item ( i ) ;
if ( node . getNodeType ( ) = = Node . ELEMENT_NODE ) {
if ( node . getTextContent ( ) . contains ( spversion . toString ( ) . split ( " \\ - " ) [ 1 ] ) & & ( mcfversion = = null | | new Version ( node . getTextContent ( ) ) . compareTo ( mcfversion ) > = 0 ) ) {
mcfversion = new Version ( node . getTextContent ( ) ) ;
}
}
}
2017-05-30 21:38:51 +02:00
if ( mcfversion = = null )
throw new InvalidServerException ( " Cannot find forge version for Sponge " + spversion . toString ( ) ) ;
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . info . println ( " Found \" forge- " + mcfversion . toString ( ) + '"' ) ;
2017-04-01 07:37:48 +02:00
host . subdata . sendPacket ( new PacketOutExLogMessage ( address , " Found \" forge- " + mcfversion . toString ( ) + '"' ) ) ;
2017-05-30 21:38:51 +02:00
version = new Version ( mcfversion . toString ( ) + " " + spversion . toString ( ) ) ;
2017-04-01 07:37:48 +02:00
}
2017-05-30 21:38:51 +02:00
} catch ( Exception e ) {
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . error . println ( e ) ;
2017-04-01 07:37:48 +02:00
}
2017-07-19 01:28:59 +02:00
if ( template . getBuildOptions ( ) . contains ( " Shell-Location " ) ) {
2017-09-24 05:19:22 +02:00
String git = ( System . getenv ( " ProgramFiles(x86) " ) = = null ) ? Pattern . compile ( " %(ProgramFiles) \\ (x86 \\ )% " , Pattern . CASE_INSENSITIVE ) . matcher ( host . host . getRawString ( " Git-Bash " ) ) . replaceAll ( " %$1% " ) : host . host . getRawString ( " Git-Bash " ) ;
String gitBash = git + ( ( git . endsWith ( File . separator ) ) ? " " : File . separator ) + " bin " + File . separatorChar + " bash.exe " ;
2017-05-30 21:38:51 +02:00
if ( ! ( System . getProperty ( " os.name " ) . toLowerCase ( ) . indexOf ( " win " ) > = 0 ) & & template . getBuildOptions ( ) . contains ( " Permission " ) ) {
2017-04-14 04:39:51 +02:00
try {
2017-05-30 21:38:51 +02:00
Process process = Runtime . getRuntime ( ) . exec ( " chmod " + template . getBuildOptions ( ) . getRawString ( " Permission " ) + ' ' + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) , null , dir ) ;
Thread . sleep ( 500 ) ;
if ( process . exitValue ( ) ! = 0 ) {
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . info . println ( " Couldn't set " + template . getBuildOptions ( ) . getRawString ( " Permission " ) + " permissions to " + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) ) ;
2017-05-30 21:38:51 +02:00
host . subdata . sendPacket ( new PacketOutExLogMessage ( address , " Couldn't set " + template . getBuildOptions ( ) . getRawString ( " Permission " ) + " permissions to " + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) ) ) ;
2017-04-14 04:39:51 +02:00
}
2017-05-30 21:38:51 +02:00
} catch ( Exception e ) {
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . info . println ( " Couldn't set " + template . getBuildOptions ( ) . getRawString ( " Permission " ) + " permissions to " + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) ) ;
2017-05-30 21:38:51 +02:00
host . subdata . sendPacket ( new PacketOutExLogMessage ( address , " Couldn't set " + template . getBuildOptions ( ) . getRawString ( " Permission " ) + " permissions to " + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) ) ) ;
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . error . println ( e ) ;
2017-05-30 21:38:51 +02:00
}
}
2017-04-01 07:37:48 +02:00
2017-05-30 21:38:51 +02:00
try {
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . info . println ( " Launching " + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) ) ;
host . subdata . sendPacket ( new PacketOutExLogMessage ( address , " Launching " + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) ) ) ;
2017-09-24 05:19:22 +02:00
thread . set ( Runtime . getRuntime ( ) . exec ( ( System . getProperty ( " os.name " ) . toLowerCase ( ) . indexOf ( " win " ) > = 0 ) ? " cmd.exe /c \" \" " + gitBash + " \" --login -i -c \" bash " + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) + ' ' + version . toString ( ) + " \" \" " : ( " bash " + template . getBuildOptions ( ) . getRawString ( " Shell-Location " ) + ' ' + version . toString ( ) + " " + System . getProperty ( " user.home " ) ) , null , dir ) ) ;
2017-07-23 06:32:57 +02:00
thread . name ( ) . file = new File ( dir , " SubCreator- " + template . getName ( ) + " - " + version . toString ( ) . replace ( " " , " @ " ) + " .log " ) ;
thread . name ( ) . process = thread . get ( ) ;
thread . name ( ) . start ( ) ;
thread . get ( ) . waitFor ( ) ;
2017-05-30 21:38:51 +02:00
Thread . sleep ( 500 ) ;
2017-07-23 06:32:57 +02:00
if ( thread . get ( ) . exitValue ( ) ! = 0 ) error = true ;
2017-07-23 23:05:33 +02:00
} catch ( InterruptedException e ) {
error = true ;
2017-05-30 21:38:51 +02:00
} catch ( Exception e ) {
error = true ;
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . error . println ( e ) ;
2017-05-30 21:38:51 +02:00
}
}
new UniversalFile ( dir , " template.yml " ) . delete ( ) ;
2017-07-23 06:32:57 +02:00
if ( error ) throw new SubCreatorException ( ) ;
return server ;
}
private void run ( String name , ServerTemplate template , Version version , int port , UUID address , String id ) {
NamedContainer < SubLogger , Process > thread = this . thread . get ( name . toLowerCase ( ) ) . get ( ) ;
UniversalFile dir = new UniversalFile ( new File ( host . host . getRawString ( " Directory " ) ) , name ) ;
dir . mkdirs ( ) ;
YAMLSection server ;
try {
server = build ( thread , dir , name , template , version , address , new LinkedList < > ( ) ) ;
generateProperties ( dir , port ) ;
generateClient ( dir , template . getType ( ) , name ) ;
} catch ( SubCreatorException e ) {
server = null ;
} catch ( Exception e ) {
server = null ;
thread . name ( ) . logger . error . println ( e ) ;
}
if ( server ! = null ) {
2017-07-19 01:28:59 +02:00
host . subdata . sendPacket ( new PacketExCreateServer ( 0 , " Created Server Successfully " , template . getConfigOptions ( ) . toJSON ( ) , id ) ) ;
2017-05-30 21:38:51 +02:00
} else {
2017-07-23 06:32:57 +02:00
thread . name ( ) . logger . info . println ( " Couldn't build the server jar. Check the SubCreator logs for more detail. " ) ;
host . subdata . sendPacket ( new PacketExCreateServer ( - 1 , " Couldn't build the server jar. Check the SubCreator logs for more detail. " , template . getConfigOptions ( ) . toJSON ( ) , id ) ) ;
2017-05-30 21:38:51 +02:00
}
2017-07-23 06:32:57 +02:00
this . thread . remove ( name . toLowerCase ( ) ) ;
2017-04-01 07:37:48 +02:00
}
2017-05-30 21:38:51 +02:00
public boolean create ( String name , ServerTemplate template , Version version , int port , UUID address , String id ) {
if ( Util . isNull ( name , template , version , port , address ) ) throw new NullPointerException ( ) ;
2017-07-23 06:32:57 +02:00
NamedContainer < Thread , NamedContainer < SubLogger , Process > > run = new NamedContainer < Thread , NamedContainer < SubLogger , Process > > ( new Thread ( ( ) - > SubCreator . this . run ( name , template , version , port , address , id ) ) , new NamedContainer < SubLogger , Process > ( new SubLogger ( null , this , name + File . separator + " Creator " , address , new Container < Boolean > ( true ) , null ) , null ) ) ;
this . thread . put ( name . toLowerCase ( ) , run ) ;
run . name ( ) . start ( ) ;
return true ;
2017-04-01 07:37:48 +02:00
}
public void terminate ( ) {
2017-07-23 06:32:57 +02:00
HashMap < String , NamedContainer < Thread , NamedContainer < SubLogger , Process > > > temp = new HashMap < String , NamedContainer < Thread , NamedContainer < SubLogger , Process > > > ( ) ;
temp . putAll ( thread ) ;
for ( String i : temp . keySet ( ) ) {
terminate ( i ) ;
}
}
2017-07-25 21:27:40 +02:00
public void terminate ( String name ) {
if ( this . thread . get ( name ) . get ( ) . get ( ) ! = null & & this . thread . get ( name ) . get ( ) . get ( ) . isAlive ( ) ) {
this . thread . get ( name ) . get ( ) . get ( ) . destroyForcibly ( ) ;
} else if ( this . thread . get ( name ) . name ( ) ! = null & & this . thread . get ( name ) . name ( ) . isAlive ( ) ) {
this . thread . get ( name ) . name ( ) . interrupt ( ) ;
this . thread . remove ( name ) ;
2017-04-01 07:37:48 +02:00
}
}
public void waitFor ( ) throws InterruptedException {
2017-07-23 06:32:57 +02:00
HashMap < String , NamedContainer < Thread , NamedContainer < SubLogger , Process > > > temp = new HashMap < String , NamedContainer < Thread , NamedContainer < SubLogger , Process > > > ( ) ;
temp . putAll ( thread ) ;
for ( String i : temp . keySet ( ) ) {
waitFor ( i ) ;
}
}
2017-07-25 21:27:40 +02:00
public void waitFor ( String name ) throws InterruptedException {
while ( this . thread . get ( name ) . name ( ) ! = null & & this . thread . get ( name ) . name ( ) . isAlive ( ) ) {
2017-04-01 07:37:48 +02:00
Thread . sleep ( 250 ) ;
}
}
2017-07-23 06:32:57 +02:00
public List < SubLogger > getLogger ( ) {
List < SubLogger > loggers = new ArrayList < SubLogger > ( ) ;
HashMap < String , NamedContainer < Thread , NamedContainer < SubLogger , Process > > > temp = new HashMap < String , NamedContainer < Thread , NamedContainer < SubLogger , Process > > > ( ) ;
temp . putAll ( thread ) ;
for ( String i : temp . keySet ( ) ) {
loggers . add ( getLogger ( i ) ) ;
}
return loggers ;
2017-04-01 07:37:48 +02:00
}
2017-07-25 21:27:40 +02:00
public SubLogger getLogger ( String name ) {
return this . thread . get ( name ) . get ( ) . name ( ) ;
2017-04-01 07:37:48 +02:00
}
2017-05-30 21:38:51 +02:00
private void generateClient ( File dir , ServerType type , String name ) throws IOException {
if ( new UniversalFile ( dir , " subservers.client " ) . exists ( ) ) {
if ( type = = ServerType . SPIGOT ) {
if ( ! new UniversalFile ( dir , " plugins " ) . exists ( ) ) new UniversalFile ( dir , " plugins " ) . mkdirs ( ) ;
Util . copyFromJar ( ExHost . class . getClassLoader ( ) , " net/ME1312/SubServers/Host/Library/Files/Client/spigot.jar " , new UniversalFile ( dir , " plugins:SubServers.Client.jar " ) . getPath ( ) ) ;
} else if ( type = = ServerType . SPONGE ) {
// TODO
2017-07-19 01:28:59 +02:00
// if (!new UniversalFile(dir, "mods").exists()) new UniversalFile(dir, "mods").mkdirs();
2017-05-30 21:38:51 +02:00
// Util.copyFromJar(SubPlugin.class.getClassLoader(), "net/ME1312/SubServers/Host/Library/Files/Client/sponge.jar", new UniversalFile(dir, "mods:SubServers.Client.jar").getPath());
}
2017-06-30 15:36:16 +02:00
JSONObject config = new JSONObject ( ) ;
2017-05-30 21:38:51 +02:00
FileWriter writer = new FileWriter ( new UniversalFile ( dir , " subservers.client " ) , false ) ;
config . put ( " Name " , name ) ;
config . put ( " Address " , host . config . get ( ) . getSection ( " Settings " ) . getSection ( " SubData " ) . getRawString ( " Address " ) ) ;
config . put ( " Password " , host . config . get ( ) . getSection ( " Settings " ) . getSection ( " SubData " ) . getRawString ( " Password " ) ) ;
2017-06-30 15:36:16 +02:00
config . put ( " Encryption " , host . config . get ( ) . getSection ( " Settings " ) . getSection ( " SubData " ) . getRawString ( " Encryption " , " NONE " ) ) ;
2017-05-30 21:38:51 +02:00
config . write ( writer ) ;
writer . close ( ) ;
2017-04-01 07:37:48 +02:00
}
}
2017-04-14 04:39:51 +02:00
private void generateProperties ( File dir , int port ) throws IOException {
File file = new File ( dir , " server.properties " ) ;
2017-05-30 21:38:51 +02:00
if ( ! file . exists ( ) ) file . createNewFile ( ) ;
2018-03-14 08:01:44 +01:00
FileInputStream is = new FileInputStream ( file ) ;
String content = Util . readAll ( new BufferedReader ( new InputStreamReader ( is ) ) ) . replace ( " server-port= " , " server-port= " + port ) . replace ( " server-ip= " , " server-ip= " + host . config . get ( ) . getSection ( " Settings " ) . getRawString ( " Server-Bind " ) ) ;
is . close ( ) ;
2017-04-14 04:39:51 +02:00
file . delete ( ) ;
PrintWriter writer = new PrintWriter ( file , " UTF-8 " ) ;
writer . write ( content ) ;
2017-04-01 07:37:48 +02:00
writer . close ( ) ;
}
}