From 030892611609908cdea2a22eff0585ac69aa9e58 Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 14 Jul 2026 15:39:55 +0900 Subject: [PATCH] refactor: Migrate MCP Gateway to Spring AI MCP Server --- McpBridge.java | 105 +------- axhub-gateway/build.gradle | 3 + .../presentation/McpRouterController.java | 90 ------- .../RegistryMcpToolSpecificationFactory.java | 124 +++++++++ .../sync/RegistryMcpToolSynchronizer.java | 87 ++++++ .../src/main/resources/application.properties | 12 + build.gradle | 1 + gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 251 ++++++++++++++++++ gradlew.bat | 36 ++- 10 files changed, 517 insertions(+), 196 deletions(-) create mode 100644 axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSpecificationFactory.java create mode 100644 axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSynchronizer.java create mode 100644 gradlew diff --git a/McpBridge.java b/McpBridge.java index 3bec94c..14e197a 100644 --- a/McpBridge.java +++ b/McpBridge.java @@ -19,71 +19,20 @@ public class McpBridge { line = line.trim(); if (line.isEmpty()) continue; - // Simple JSON parsing by finding method names - // Using a rudimentary JSON parser since we don't have Jackson here - if (line.contains("\"method\":\"initialize\"") || line.contains("\"method\": \"initialize\"")) { - String id = extractId(line); - String resp = "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"axhub-gateway\",\"version\":\"1.0.0\"}}}"; - System.out.println(resp); + try { + HttpRequest req = HttpRequest.newBuilder() + // Spring AI MCP Server의 공식 단일 엔드포인트 + .uri(URI.create("http://localhost:8081/mcp")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(line)) + .build(); + + HttpResponse response = client.send(req, HttpResponse.BodyHandlers.ofString()); + System.out.println(response.body()); System.out.flush(); - } else if (line.contains("\"method\":\"notifications/initialized\"") || line.contains("\"method\": \"notifications/initialized\"")) { - // Do nothing - } else if (line.contains("\"method\":\"tools/list\"") || line.contains("\"method\": \"tools/list\"")) { - String id = extractId(line); - try { - HttpRequest req = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:8281/mcp/api/v1/tools/list")) - - .GET() - .build(); - HttpResponse response = client.send(req, HttpResponse.BodyHandlers.ofString()); - - // Parse tools from response string manually (basic string manipulation) - // axhub-gateway returns: {"result":{"tools":[{"subToolName":"other_get_sample_string","description":"...","parametersSchema":{...}}]}} - // We need to format it to standard MCP format - String rawJson = response.body(); - // For simplicity, we can just proxy the response if it matches closely, but it doesn't. - // Let's just do a naive conversion by replacing "subToolName" with "name" and "parametersSchema" with "inputSchema" - String transformed = rawJson; - // Add type: "object" to inputSchema if missing - too complex with string replace, let's hope axhub-gateway already provides correct schema - - // The result format expects: {"tools": [ ... ]} - // axhub-gateway returns: {"id":"...","result":{"tools":[...]}} - // So we extract the "result" object and wrap it in the JSON-RPC response - int resultIdx = transformed.indexOf("\"result\":{"); - if (resultIdx != -1) { - String resultObj = transformed.substring(resultIdx + 9, transformed.lastIndexOf("}") ); - System.out.println("{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":" + resultObj + "}"); - } else { - System.out.println("{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"tools\":[]}}"); - } - } catch (Exception e) { - System.out.println("{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"error\":{\"code\":-32603,\"message\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}}"); - } - System.out.flush(); - } else if (line.contains("\"method\":\"tools/call\"") || line.contains("\"method\": \"tools/call\"")) { - String id = extractId(line); - try { - HttpRequest req = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:8281/mcp/api/v1/tools/call")) - .header("Content-Type", "application/json") - .header("X-Agent-Id", "Antigravity") - - .POST(HttpRequest.BodyPublishers.ofString(line)) - .build(); - HttpResponse response = client.send(req, HttpResponse.BodyHandlers.ofString()); - - String rawJson = response.body(); - // The gateway returns {"jsonrpc":"2.0", "result": {...}, "error": {...}} - // Standard MCP expects {"content": [{"type": "text", "text": "..."}], "isError": false} - - boolean isError = rawJson.contains("\"error\":") || rawJson.contains("\"status\":\"ERROR\""); - String escapedRawJson = rawJson.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", ""); - - System.out.println("{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"" + escapedRawJson + "\"}],\"isError\":" + isError + "}}"); - } catch (Exception e) { - System.out.println("{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}],\"isError\":true}}"); - } + } catch (Exception e) { + // MCP 표준 에러 포맷으로 반환 (임의로 id 추출 제외, 최소한의 에러 응답) + System.out.println("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}}"); System.out.flush(); } } @@ -91,34 +40,6 @@ public class McpBridge { e.printStackTrace(); } } - - private static String extractId(String line) { - int idx = line.indexOf("\"id\":"); - if (idx == -1) return "null"; - int start = idx + 5; - while (start < line.length() && line.charAt(start) == ' ') { - start++; - } - if (start >= line.length()) return "null"; - - int end = start; - if (line.charAt(start) == '\"') { - end = start + 1; - while (end < line.length() && line.charAt(end) != '\"') { - end++; - } - if (end < line.length()) { - end++; // include closing quote - } - } else { - while (end < line.length() && Character.isDigit(line.charAt(end))) { - end++; - } - } - - if (start == end) return "null"; - return line.substring(start, end); - } } diff --git a/axhub-gateway/build.gradle b/axhub-gateway/build.gradle index ee1815f..4e74997 100644 --- a/axhub-gateway/build.gradle +++ b/axhub-gateway/build.gradle @@ -8,6 +8,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // Spring AI MCP Server + implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' + // MyBatis & DB implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' diff --git a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/presentation/McpRouterController.java b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/presentation/McpRouterController.java index ca7680b..5d9a141 100644 --- a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/presentation/McpRouterController.java +++ b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/presentation/McpRouterController.java @@ -60,97 +60,7 @@ public class McpRouterController { this.restClient = RestClient.builder().requestFactory(factory).build(); } - @Operation(summary = "MCP 파이프라인 호출", description = "MCP 표준 파이프라인(ExecuteService)을 통해 레거시 툴을 호출합니다.") - @PostMapping("/tools/call") - public ResponseEntity callTool( - @RequestHeader(value = "X-Trace-Id", required = false) String traceId, - @RequestHeader(value = "X-Agent-Id", required = false) String agentId, - @RequestHeader(value = "X-User-Prompt", required = false) String userPrompt, - @RequestBody Map payload, - @RequestAttribute(value = "tenantId", required = false) String tenantId) { - - Map meta = new HashMap<>(); - meta.put("traceId", traceId != null ? traceId : UUID.randomUUID().toString()); - meta.put("agentId", agentId != null ? agentId : "UNKNOWN"); - meta.put("userPrompt", userPrompt != null ? userPrompt : ""); - payload.put("meta", meta); - - log.info("[MCP 표준] ExecuteService 파이프라인을 통한 툴 호출 시작 (Tenant: {}, Trace: {})", tenantId, meta.get("traceId")); - - try { - log.info("[AI -> MCP Gateway] 동적 툴 실행 요청 수신: {}", objectMapper.writeValueAsString(payload)); - } catch (Exception ex) { - log.info("[AI -> MCP Gateway] 동적 툴 실행 요청 수신: {}", payload); - } - try { - Object result = executeService.execute(payload, tenantId); - - try { - log.info("[MCP Gateway -> AI] 동적 툴 실행 결과 반환: {}", objectMapper.writeValueAsString(result)); - } catch (Exception ex) { - log.info("[MCP Gateway -> AI] 동적 툴 실행 결과 반환: {}", result); - } - - return ResponseEntity.ok(result); - } catch (SecurityException se) { - log.warn(" [보안 차단] 권한 오류: {}", se.getMessage()); - return ResponseEntity.status(403).body(Map.of("error", se.getMessage())); - } catch (Exception e) { - log.error(" 파이프라인 실행 중 오류: {}", e.getMessage()); - return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); - } - } - - @GetMapping("/tools/list") - public ResponseEntity listTools( - @RequestParam(value = "categoryKey", required = false) String categoryKey) { - List activeTools = redisRegistryService.getAllTools() - .stream() - .filter(ToolMetadata::getVisible) - .collect(Collectors.toList()); - - Set knownTools = activeTools.stream() - .map(ToolMetadata::getUid) - .collect(Collectors.toSet()); - - Set fallbackUrls = new HashSet<>(gatewayFallbackProperties.getRoutes().values()); - if (gatewayFallbackProperties.getDefaultUrl() != null) { - fallbackUrls.add(gatewayFallbackProperties.getDefaultUrl()); - } - - for (String url : fallbackUrls) { - try { - List localTools = restClient.get() - .uri(url + "/mcp/api/v1/tools/local") - .retrieve() - .body(new ParameterizedTypeReference>() {}); - - if (localTools != null) { - for (ToolMetadata t : localTools) { - if (!Boolean.TRUE.equals(t.getIsRegistered()) && Boolean.TRUE.equals(t.getVisible()) && !knownTools.contains(t.getUid())) { - activeTools.add(t); - knownTools.add(t.getUid()); - } - } - } - } catch (Exception e) { - log.warn("Failed to fetch local tools from fallback URL: {}", url); - } - } - - if (categoryKey != null && !categoryKey.trim().isEmpty()) { - activeTools = activeTools.stream() - .filter(t -> categoryKey.equals(t.getCategoryKey())) - .collect(Collectors.toList()); - } - - JsonRpcResponse response = new JsonRpcResponse(); - response.setId(UUID.randomUUID().toString()); - response.setResult(Map.of("tools", activeTools)); - - return ResponseEntity.ok(response); - } @GetMapping(value = "/tools/docs/markdown", produces = "text/markdown;charset=UTF-8") public ResponseEntity generateToolsMarkdown() { diff --git a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSpecificationFactory.java b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSpecificationFactory.java new file mode 100644 index 0000000..815f569 --- /dev/null +++ b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSpecificationFactory.java @@ -0,0 +1,124 @@ +package io.shinhanlife.axhub.biz.mcp.gateway.sync; + +import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; +import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService; +import io.modelcontextprotocol.server.McpStatelessServerFeatures; +import io.modelcontextprotocol.spec.McpSchema; +import org.springframework.stereotype.Component; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Registry에 등록된 실제 Tool 메타데이터를 MCP 표준 Tool specification으로 변환합니다. + */ +@Component +public class RegistryMcpToolSpecificationFactory { + + private final ExecuteService executeService; + private final ObjectMapper objectMapper; + + public RegistryMcpToolSpecificationFactory(ExecuteService executeService, ObjectMapper objectMapper) { + this.executeService = executeService; + this.objectMapper = objectMapper; + } + + /** + * Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다. + */ + public McpStatelessServerFeatures.SyncToolSpecification create(ToolMetadata entry) { + McpSchema.Tool tool = McpSchema.Tool.builder() + .name(entry.getName()) + .description(description(entry)) + .inputSchema(inputSchema(entry)) + .build(); + + return McpStatelessServerFeatures.SyncToolSpecification.builder() + .tool(tool) + .callHandler((context, request) -> execute(entry.getName(), request)) + .build(); + } + + private McpSchema.CallToolResult execute(String toolName, McpSchema.CallToolRequest request) { + try { + // Build legacy JSON-RPC payload format expected by ExecuteService + Map payload = new HashMap<>(); + payload.put("jsonrpc", "2.0"); + payload.put("method", "tools/call"); + payload.put("id", UUID.randomUUID().toString()); + + Map params = new HashMap<>(); + params.put("name", toolName); + params.put("arguments", request.arguments()); + payload.put("params", params); + + // Execute through ExecuteService + Object rawResult = executeService.execute(payload, "system"); + + // Convert JSON-RPC response back to McpSchema.CallToolResult + return toCallToolResult(rawResult); + } catch (Exception error) { + return McpSchema.CallToolResult.builder() + .addTextContent("Tool 실행 중 내부 오류가 발생했습니다: " + error.getMessage()) + .isError(true) + .build(); + } + } + + private McpSchema.CallToolResult toCallToolResult(Object rawResult) { + try { + Map resultMap = objectMapper.convertValue(rawResult, new TypeReference>() {}); + Object innerResult = resultMap.get("result"); + + McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder(); + builder.isError(resultMap.containsKey("error")); + + if (innerResult instanceof Map) { + Map innerMap = (Map) innerResult; + // If it follows structured content + if (innerMap.containsKey("content")) { + List> contentList = (List>) innerMap.get("content"); + for (Map item : contentList) { + if ("text".equals(item.get("type"))) { + builder.addTextContent((String) item.get("text")); + } + } + } else { + // Fallback to text string representation + builder.addTextContent(objectMapper.writeValueAsString(innerResult)); + } + } else { + builder.addTextContent(String.valueOf(innerResult)); + } + return builder.build(); + } catch (Exception e) { + return McpSchema.CallToolResult.builder() + .addTextContent(String.valueOf(rawResult)) + .build(); + } + } + + private Map inputSchema(ToolMetadata entry) { + if (entry.getParametersSchema() != null) { + return entry.getParametersSchema(); + } + + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", new HashMap<>()); + schema.put("additionalProperties", false); + return schema; + } + + private String description(ToolMetadata entry) { + return entry.getDescription() == null || entry.getDescription().isBlank() + ? entry.getName() + " Tool" + : entry.getDescription(); + } +} diff --git a/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSynchronizer.java b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSynchronizer.java new file mode 100644 index 0000000..63e6bf6 --- /dev/null +++ b/axhub-gateway/src/main/java/io/shinhanlife/axhub/biz/mcp/gateway/sync/RegistryMcpToolSynchronizer.java @@ -0,0 +1,87 @@ +package io.shinhanlife.axhub.biz.mcp.gateway.sync; + +import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; +import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService; +import io.modelcontextprotocol.server.McpStatelessSyncServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +/** + * MCP 서버의 tools/list 노출 목록을 Redis Registry의 ACTIVE Tool 목록과 동기화합니다. + */ +@Service +public class RegistryMcpToolSynchronizer { + private static final Logger log = LoggerFactory.getLogger(RegistryMcpToolSynchronizer.class); + + private final ObjectProvider mcpServerProvider; + private final RedisRegistryService redisRegistryService; + private final RegistryMcpToolSpecificationFactory specificationFactory; + private final Set managedToolNames = new LinkedHashSet<>(); + private final ReentrantLock lock = new ReentrantLock(); + + public RegistryMcpToolSynchronizer(ObjectProvider mcpServerProvider, + RedisRegistryService redisRegistryService, + RegistryMcpToolSpecificationFactory specificationFactory) { + this.mcpServerProvider = mcpServerProvider; + this.redisRegistryService = redisRegistryService; + this.specificationFactory = specificationFactory; + } + + @EventListener(ApplicationReadyEvent.class) + public void synchronizeOnStartup() { + synchronize(); + } + + @Scheduled(fixedDelayString = "${mcp.tool-registry-sync-interval-millis:5000}") + public void synchronizeScheduled() { + synchronize(); + } + + public void synchronize() { + McpStatelessSyncServer server = mcpServerProvider.getIfAvailable(); + if (server == null) { + return; + } + + lock.lock(); + try { + List activeEntries = redisRegistryService.getAllTools() + .stream().filter(ToolMetadata::getVisible).collect(Collectors.toList()); + + Set activeNames = activeEntries.stream() + .map(ToolMetadata::getName) // We use 'name' for MCP Tool Name + .collect(Collectors.toCollection(LinkedHashSet::new)); + + Set removedNames = new LinkedHashSet<>(managedToolNames); + removedNames.removeAll(activeNames); + for (String removedName : removedNames) { + server.removeTool(removedName); + managedToolNames.remove(removedName); + log.info("Removed MCP registry tool. tool={}", removedName); + } + + for (ToolMetadata entry : activeEntries) { + if (managedToolNames.contains(entry.getName())) { + server.removeTool(entry.getName()); + } + server.addTool(specificationFactory.create(entry)); + managedToolNames.add(entry.getName()); + } + } catch (Exception error) { + log.warn("MCP registry tool synchronization failed. message={}", error.getMessage()); + } finally { + lock.unlock(); + } + } +} diff --git a/axhub-gateway/src/main/resources/application.properties b/axhub-gateway/src/main/resources/application.properties index 32d8c90..7dee1b4 100644 --- a/axhub-gateway/src/main/resources/application.properties +++ b/axhub-gateway/src/main/resources/application.properties @@ -28,3 +28,15 @@ server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=20s # Suppress Kafka Connection Logs logging.level.org.apache.kafka=ERROR + +# Spring AI MCP Server Settings +spring.ai.mcp.server.protocol=STATELESS +mcp.agent-claims-required=false +mcp.trusted-claims-required=false +mcp.write-approval-required=false +mcp.default-page-size=100 +mcp.max-page-size=500 +mcp.max-pages-per-call=3 +mcp.max-stream-bytes=1048576 +mcp.max-response-bytes-from-tool=5242880 +mcp.tool-registry-sync-interval-millis=5000 diff --git a/build.gradle b/build.gradle index c2a28dd..a5d5626 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ subprojects { dependencyManagement { imports { mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES + mavenBom "org.springframework.ai:spring-ai-bom:2.0.0" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 598c78d..d4081da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..0f14772 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 7e60b72..8de1053 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem gradlew startup script for Windows +@rem Gradle startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -"%COMSPEC%" /c exit 1 +goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,18 +65,30 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -"%COMSPEC%" /c exit 1 +goto fail :execute @rem Setup the command line +set CLASSPATH= -@rem Execute gradlew -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega