@archznn/xavva 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/package.json +1 -1
- package/src/commands/Command.ts +1 -1
- package/src/commands/CommandRegistry.ts +3 -3
- package/src/commands/DeployCommand.ts +4 -3
- package/src/commands/HelpCommand.ts +6 -3
- package/src/commands/TomcatCommand.ts +105 -10
- package/src/index.ts +1 -1
- package/src/services/BuildService.ts +105 -3
- package/src/services/EmbeddedTomcatService.ts +415 -370
- package/src/services/WatcherService.ts +41 -3
- package/src/types/config.ts +1 -0
- package/src/utils/config.ts +3 -1
- package/src/utils/constants.ts +2 -2
|
@@ -1,245 +1,288 @@
|
|
|
1
1
|
import { Logger } from "../utils/ui";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
createWriteStream,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
promises as fsPromises,
|
|
9
|
+
} from "fs";
|
|
3
10
|
import path from "path";
|
|
4
11
|
import os from "os";
|
|
5
12
|
import { spawn } from "child_process";
|
|
6
13
|
|
|
7
14
|
export interface EmbeddedTomcatOptions {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
version?: string;
|
|
16
|
+
port?: number;
|
|
17
|
+
webappPath: string;
|
|
18
|
+
contextPath?: string;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
interface DownloadProgress {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
downloaded: number;
|
|
23
|
+
total: number;
|
|
24
|
+
percent: number;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export class EmbeddedTomcatService {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
28
|
+
private readonly baseDir: string;
|
|
29
|
+
private readonly version: string;
|
|
30
|
+
private port: number;
|
|
31
|
+
private webappPath: string;
|
|
32
|
+
private contextPath: string;
|
|
33
|
+
private tomcatHome: string;
|
|
34
|
+
private downloadUrl: string;
|
|
35
|
+
private isInstalled: boolean = false;
|
|
36
|
+
|
|
37
|
+
// Versões estáveis do Tomcat (atualizadas: 2026-03-04)
|
|
38
|
+
private static readonly VERSIONS: Record<
|
|
39
|
+
string,
|
|
40
|
+
{ url: string; sha512: string }
|
|
41
|
+
> = {
|
|
42
|
+
"10.1.52": {
|
|
43
|
+
url: "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.52/bin/apache-tomcat-10.1.52-windows-x64.zip",
|
|
44
|
+
sha512: "",
|
|
45
|
+
},
|
|
46
|
+
"9.0.115": {
|
|
47
|
+
url: "https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.115/bin/apache-tomcat-9.0.115-windows-x64.zip",
|
|
48
|
+
sha512: "",
|
|
49
|
+
},
|
|
50
|
+
"11.0.18": {
|
|
51
|
+
url: "https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.18/bin/apache-tomcat-11.0.18-windows-x64.zip",
|
|
52
|
+
sha512: "",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
constructor(options: EmbeddedTomcatOptions) {
|
|
57
|
+
this.version = options.version || "10.1.52";
|
|
58
|
+
this.port = options.port || 8080;
|
|
59
|
+
this.webappPath = path.resolve(options.webappPath);
|
|
60
|
+
this.contextPath = options.contextPath || "/";
|
|
61
|
+
this.baseDir = path.join(os.homedir(), ".xavva", "tomcat");
|
|
62
|
+
this.tomcatHome = path.join(this.baseDir, this.version);
|
|
63
|
+
|
|
64
|
+
// Se a versão não está na lista, usa URL padrão
|
|
65
|
+
const versionInfo = EmbeddedTomcatService.VERSIONS[this.version];
|
|
66
|
+
if (versionInfo) {
|
|
67
|
+
this.downloadUrl = versionInfo.url;
|
|
68
|
+
} else {
|
|
69
|
+
// Tenta inferir URL baseado no padrão Apache
|
|
70
|
+
const majorVersion = this.version.split(".")[0];
|
|
71
|
+
this.downloadUrl = `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${this.version}/bin/apache-tomcat-${this.version}-windows-x64.zip`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Verifica se o Tomcat já está instalado
|
|
77
|
+
*/
|
|
78
|
+
checkInstallation(): boolean {
|
|
79
|
+
const catalinaBat = path.join(this.tomcatHome, "bin", "catalina.bat");
|
|
80
|
+
this.isInstalled = existsSync(catalinaBat);
|
|
81
|
+
return this.isInstalled;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retorna o caminho do Tomcat (instalado ou para instalar)
|
|
86
|
+
*/
|
|
87
|
+
getTomcatHome(): string {
|
|
88
|
+
return this.tomcatHome;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Lista todas as versões instaladas
|
|
93
|
+
*/
|
|
94
|
+
static listInstalledVersions(): string[] {
|
|
95
|
+
const baseDir = path.join(os.homedir(), ".xavva", "tomcat");
|
|
96
|
+
if (!existsSync(baseDir)) return [];
|
|
97
|
+
|
|
98
|
+
const versions: string[] = [];
|
|
99
|
+
const entries = readdirSync(baseDir, { withFileTypes: true });
|
|
100
|
+
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
const catalinaBat = path.join(
|
|
104
|
+
baseDir,
|
|
105
|
+
entry.name,
|
|
106
|
+
"bin",
|
|
107
|
+
"catalina.bat",
|
|
108
|
+
);
|
|
109
|
+
if (existsSync(catalinaBat)) {
|
|
110
|
+
versions.push(entry.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return versions.sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Baixa e instala o Tomcat
|
|
120
|
+
*/
|
|
121
|
+
async install(): Promise<boolean> {
|
|
122
|
+
if (this.checkInstallation()) {
|
|
123
|
+
Logger.info("Tomcat", `Versão ${this.version} já instalada`);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
Logger.section("Instalando Tomcat Embutido");
|
|
128
|
+
Logger.info("Versão", this.version);
|
|
129
|
+
Logger.info("Destino", this.tomcatHome);
|
|
130
|
+
|
|
131
|
+
// Cria diretório base
|
|
132
|
+
if (!existsSync(this.baseDir)) {
|
|
133
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const zipPath = path.join(
|
|
137
|
+
this.baseDir,
|
|
138
|
+
`apache-tomcat-${this.version}.zip`,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Download
|
|
143
|
+
await this.downloadFile(this.downloadUrl, zipPath);
|
|
144
|
+
|
|
145
|
+
// Extração
|
|
146
|
+
await this.extractZip(zipPath, this.baseDir);
|
|
147
|
+
|
|
148
|
+
// Renomeia diretório extraído para versão padronizada
|
|
149
|
+
const extractedDir = path.join(
|
|
150
|
+
this.baseDir,
|
|
151
|
+
`apache-tomcat-${this.version}`,
|
|
152
|
+
);
|
|
153
|
+
if (existsSync(extractedDir) && extractedDir !== this.tomcatHome) {
|
|
154
|
+
await fsPromises.rename(extractedDir, this.tomcatHome);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Limpa arquivo zip
|
|
158
|
+
await fsPromises.unlink(zipPath).catch(() => { });
|
|
159
|
+
|
|
160
|
+
// Configura server.xml
|
|
161
|
+
await this.configureServerXml();
|
|
162
|
+
|
|
163
|
+
// Configura context.xml para hot-reload
|
|
164
|
+
await this.configureContextXml();
|
|
165
|
+
|
|
166
|
+
this.isInstalled = true;
|
|
167
|
+
Logger.success(`Tomcat ${this.version} instalado com sucesso!`);
|
|
168
|
+
return true;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
Logger.error(`Falha ao instalar Tomcat: ${error}`);
|
|
171
|
+
// Limpa arquivos parciais
|
|
172
|
+
if (existsSync(this.tomcatHome)) {
|
|
173
|
+
await fsPromises.rm(this.tomcatHome, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Configura server.xml com porta personalizada
|
|
181
|
+
*/
|
|
182
|
+
private async configureServerXml(): Promise<void> {
|
|
183
|
+
const serverXmlPath = path.join(this.tomcatHome, "conf", "server.xml");
|
|
184
|
+
|
|
185
|
+
if (!existsSync(serverXmlPath)) {
|
|
186
|
+
throw new Error("server.xml não encontrado após extração");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let content = await fsPromises.readFile(serverXmlPath, "utf-8");
|
|
190
|
+
|
|
191
|
+
// Atualiza porta HTTP
|
|
192
|
+
content = content.replace(
|
|
193
|
+
/<Connector port="8080"/,
|
|
194
|
+
`<Connector port="${this.port}"`,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Atualiza porta de shutdown
|
|
198
|
+
const shutdownPort = this.port + 1000;
|
|
199
|
+
content = content.replace(
|
|
200
|
+
/<Server port="8005"/,
|
|
201
|
+
`<Server port="${shutdownPort}"`,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Atualiza porta AJP (se existir)
|
|
205
|
+
content = content.replace(
|
|
206
|
+
/<Connector port="8009"/,
|
|
207
|
+
`<Connector port="${this.port + 1001}"`,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Desabilita manager e host-manager em embedded (opcional)
|
|
211
|
+
// Remove context do manager para segurança
|
|
212
|
+
content = content.replace(
|
|
213
|
+
/<Context docBase="manager"[^>]*\/>/g,
|
|
214
|
+
'<!-- <Context docBase="manager" ... /> -->',
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
await fsPromises.writeFile(serverXmlPath, content, "utf-8");
|
|
218
|
+
Logger.debug(`server.xml configurado na porta ${this.port}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Configura context.xml para hot-reload
|
|
223
|
+
*/
|
|
224
|
+
private async configureContextXml(): Promise<void> {
|
|
225
|
+
const contextXmlPath = path.join(this.tomcatHome, "conf", "context.xml");
|
|
226
|
+
|
|
227
|
+
if (!existsSync(contextXmlPath)) return;
|
|
228
|
+
|
|
229
|
+
let content = await fsPromises.readFile(contextXmlPath, "utf-8");
|
|
230
|
+
|
|
231
|
+
// Adiciona atributos para hot-reload se não existirem
|
|
232
|
+
if (!content.includes("reloadable")) {
|
|
233
|
+
content = content.replace(
|
|
234
|
+
/<Context>/,
|
|
235
|
+
'<Context reloadable="true" autoDeploy="true" deployOnStartup="true">',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await fsPromises.writeFile(contextXmlPath, content, "utf-8");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Cria contexto para a aplicação
|
|
244
|
+
*/
|
|
245
|
+
async createAppContext(): Promise<void> {
|
|
246
|
+
const webappsDir = path.join(this.tomcatHome, "webapps");
|
|
247
|
+
|
|
248
|
+
// Limpa webapps padrão
|
|
249
|
+
const defaultApps = ["docs", "examples", "host-manager", "manager", "ROOT"];
|
|
250
|
+
for (const app of defaultApps) {
|
|
251
|
+
const appPath = path.join(webappsDir, app);
|
|
252
|
+
if (existsSync(appPath)) {
|
|
253
|
+
await fsPromises.rm(appPath, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Cria diretório para a aplicação
|
|
258
|
+
const appName =
|
|
259
|
+
this.contextPath === "/" ? "ROOT" : this.contextPath.replace(/^\//, "");
|
|
260
|
+
const appDir = path.join(webappsDir, appName);
|
|
261
|
+
|
|
262
|
+
if (existsSync(appDir)) {
|
|
263
|
+
await fsPromises.rm(appDir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Se webappPath é um diretório, cria link/simula deploy
|
|
267
|
+
if (existsSync(this.webappPath)) {
|
|
268
|
+
// Em Windows, vamos copiar inicialmente (symlink requer privilégios)
|
|
269
|
+
// Ou criar um context XML apontando para o diretório
|
|
270
|
+
await this.createContextXml(appName);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Cria arquivo context XML para apontar para diretório externo
|
|
276
|
+
*/
|
|
277
|
+
private async createContextXml(appName: string): Promise<void> {
|
|
278
|
+
const confDir = path.join(this.tomcatHome, "conf", "Catalina", "localhost");
|
|
279
|
+
|
|
280
|
+
if (!existsSync(confDir)) {
|
|
281
|
+
mkdirSync(confDir, { recursive: true });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const contextFile = path.join(confDir, `${appName}.xml`);
|
|
285
|
+
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
243
286
|
<Context
|
|
244
287
|
docBase="${this.webappPath.replace(/\\/g, "/")}"
|
|
245
288
|
reloadable="true"
|
|
@@ -248,144 +291,146 @@ export class EmbeddedTomcatService {
|
|
|
248
291
|
antiJARLocking="false">
|
|
249
292
|
</Context>`;
|
|
250
293
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
294
|
+
writeFileSync(contextFile, content);
|
|
295
|
+
Logger.debug(`Context criado: ${contextFile}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Verifica se porta está disponível
|
|
300
|
+
*/
|
|
301
|
+
async isPortAvailable(): Promise<boolean> {
|
|
302
|
+
return new Promise((resolve) => {
|
|
303
|
+
const netstat = spawn("cmd", [
|
|
304
|
+
"/c",
|
|
305
|
+
`netstat -ano | findstr :${this.port}`,
|
|
306
|
+
]);
|
|
307
|
+
let output = "";
|
|
308
|
+
|
|
309
|
+
netstat.stdout?.on("data", (data) => {
|
|
310
|
+
output += data.toString();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
netstat.on("close", () => {
|
|
314
|
+
resolve(output.trim().length === 0);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
netstat.on("error", () => {
|
|
318
|
+
resolve(true); // Assume disponível se não conseguir verificar
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Encontra próxima porta disponível
|
|
325
|
+
*/
|
|
326
|
+
async findAvailablePort(startPort: number = 8080): Promise<number> {
|
|
327
|
+
let port = startPort;
|
|
328
|
+
while (!(await this.isPortAvailable())) {
|
|
329
|
+
port++;
|
|
330
|
+
if (port > 65535) {
|
|
331
|
+
throw new Error("Nenhuma porta disponível encontrada");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.port = port;
|
|
335
|
+
return port;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Retorna variáveis de ambiente para o Tomcat
|
|
340
|
+
*/
|
|
341
|
+
getEnvironment(): Record<string, string> {
|
|
342
|
+
return {
|
|
343
|
+
CATALINA_HOME: this.tomcatHome,
|
|
344
|
+
CATALINA_BASE: this.tomcatHome,
|
|
345
|
+
CATALINA_OPTS: process.env.CATALINA_OPTS || "",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Lista versões disponíveis
|
|
351
|
+
*/
|
|
352
|
+
static getAvailableVersions(): string[] {
|
|
353
|
+
return Object.keys(EmbeddedTomcatService.VERSIONS);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Download com progresso
|
|
358
|
+
*/
|
|
359
|
+
private async downloadFile(url: string, destPath: string): Promise<void> {
|
|
360
|
+
const spinner = Logger.spinner(`Baixando Tomcat ${this.version}...`);
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const response = await fetch(url);
|
|
364
|
+
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const totalSize = parseInt(response.headers.get("content-length") || "0");
|
|
370
|
+
const buffer = await response.arrayBuffer();
|
|
371
|
+
|
|
372
|
+
writeFileSync(destPath, Buffer.from(buffer));
|
|
373
|
+
|
|
374
|
+
spinner(true);
|
|
375
|
+
|
|
376
|
+
const sizeMB = (buffer.byteLength / 1024 / 1024).toFixed(1);
|
|
377
|
+
Logger.info("Download", `${sizeMB} MB baixados`);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
spinner(false);
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Extrai arquivo ZIP usando PowerShell
|
|
386
|
+
*/
|
|
387
|
+
private async extractZip(zipPath: string, destDir: string): Promise<void> {
|
|
388
|
+
const spinner = Logger.spinner("Extraindo arquivos...");
|
|
389
|
+
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
391
|
+
const ps = spawn("powershell", [
|
|
392
|
+
"-command",
|
|
393
|
+
`Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`,
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
ps.on("close", (code) => {
|
|
397
|
+
if (code === 0) {
|
|
398
|
+
spinner(true);
|
|
399
|
+
resolve();
|
|
400
|
+
} else {
|
|
401
|
+
spinner(false);
|
|
402
|
+
reject(new Error(`Falha ao extrair (código ${code})`));
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
ps.on("error", (err) => {
|
|
407
|
+
spinner(false);
|
|
408
|
+
reject(err);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Remove instalação
|
|
415
|
+
*/
|
|
416
|
+
async uninstall(): Promise<void> {
|
|
417
|
+
if (existsSync(this.tomcatHome)) {
|
|
418
|
+
await fsPromises.rm(this.tomcatHome, { recursive: true, force: true });
|
|
419
|
+
Logger.info("Tomcat", `Versão ${this.version} removida`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Retorna informações da instalação
|
|
425
|
+
*/
|
|
426
|
+
getInfo(): Record<string, string> {
|
|
427
|
+
return {
|
|
428
|
+
version: this.version,
|
|
429
|
+
home: this.tomcatHome,
|
|
430
|
+
port: String(this.port),
|
|
431
|
+
installed: this.isInstalled ? "sim" : "não",
|
|
432
|
+
webapp: this.webappPath,
|
|
433
|
+
context: this.contextPath,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
391
436
|
}
|