Croway commented on code in PR #21534:
URL: https://github.com/apache/camel/pull/21534#discussion_r2826742966


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java:
##########
@@ -0,0 +1,945 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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.
+ */
+package org.apache.camel.dsl.jbang.core.commands;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Stack;
+import java.util.concurrent.ThreadLocalRandom;
+
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Page.NavigateOptions;
+import com.microsoft.playwright.Page.ScreenshotOptions;
+import com.microsoft.playwright.Page.WaitForFunctionOptions;
+import com.microsoft.playwright.Page.WaitForSelectorOptions;
+import com.microsoft.playwright.Playwright;
+import com.microsoft.playwright.options.ViewportSize;
+import com.microsoft.playwright.options.WaitUntilState;
+import org.apache.camel.dsl.jbang.core.commands.process.Hawtio;
+import org.apache.camel.dsl.jbang.core.commands.process.Jolokia;
+import org.apache.camel.dsl.jbang.core.commands.process.StopProcess;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.StopWatch;
+import picocli.CommandLine;
+
[email protected](name = "diagram", description = "Visualize Camel routes 
using Hawtio", sortOptions = false,
+                     showDefaultValues = true)
+public class Diagram extends CamelCommand {
+
+    @CommandLine.Parameters(description = "The Camel file(s) to run. If no 
files specified then use --name to attach to a running integration.",
+                            arity = "0..9", paramLabel = "<files>", 
parameterConsumer = FilesConsumer.class)
+    Path[] filePaths; // Defined only for file path completion; the field 
never used
+    List<String> files = new ArrayList<>();
+
+    @CommandLine.Option(names = { "--name" },
+                        description = "Name or pid of running Camel 
integration")
+    String name;
+
+    @CommandLine.Option(names = { "--renderer" },
+                        description = "Renderer to use (hawtio)",
+                        defaultValue = "hawtio")
+    String renderer = "hawtio";
+
+    @CommandLine.Option(names = { "--port" },
+                        description = "Port number to use for Hawtio web 
console (port 8888 by default)", defaultValue = "8888")
+    int port = 8888;
+
+    @CommandLine.Option(names = { "--openUrl" },
+                        description = "To automatic open Hawtio web console in 
the web browser", defaultValue = "true")
+    boolean openUrl = true;
+
+    @CommandLine.Option(names = { "--background-wait" }, defaultValue = "true",
+                        description = "To wait for run in background to 
startup successfully, before returning")
+    boolean backgroundWait = true;
+
+    @CommandLine.Option(names = { "--keep-running" }, defaultValue = "false",
+                        description = "Keep the background Camel integration 
running after exiting Hawtio")
+    boolean keepRunning;
+
+    @CommandLine.Option(names = { "--output" },
+                        description = "Write a PNG snapshot of the route 
diagram to the given file")
+    Path output;
+
+    @CommandLine.Option(names = { "--browser" },
+                        description = "Playwright browser to use (chromium 
only)",
+                        defaultValue = "chromium")
+    String browser = "chromium";
+
+    @CommandLine.Option(names = { "--playwright-browser-path" },
+                        description = "Path to the Playwright browser 
executable")
+    String playwrightBrowserPath;
+
+    @CommandLine.Option(names = { "--playwright-timeout" }, defaultValue = 
"120000",
+                        description = "Timeout in millis for Playwright 
navigation and rendering")
+    long playwrightTimeout = 120000;
+
+    @CommandLine.Option(names = { "--route-id" },
+                        description = "Route id to render (defaults to the 
first route)")
+    String routeId;
+
+    @CommandLine.Option(names = { "--jolokia-port" }, defaultValue = "8778",
+                        description = "Jolokia port to attach when exporting 
PNG")
+    int jolokiaPort = 8778;
+
+    private boolean jolokiaAttached;
+
+    public Diagram(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        String selectedRenderer = renderer == null ? "hawtio" : 
renderer.toLowerCase(Locale.ROOT);
+        if (!"hawtio".equals(selectedRenderer)) {
+            printer().printErr("Unsupported renderer: " + renderer);
+            return 1;
+        }
+
+        boolean hasFiles = files != null && !files.isEmpty();
+        boolean exportPng = output != null;
+        if (exportPng && openUrl) {
+            openUrl = false;
+        }
+
+        String runName = name;
+        if (hasFiles && (runName == null || runName.isBlank())) {
+            runName = FileUtil.onlyName(FileUtil.stripPath(files.get(0)));
+        }
+        String target = runName;
+        if (!hasFiles && (target == null || target.isBlank())) {
+            new CommandLine(this).execute("--help");
+            return 0;
+        }
+
+        long pid = 0;
+        boolean started = false;
+        int exit = 0;
+        try {
+            if (hasFiles) {
+                Run run = new Run(getMain());
+                run.backgroundWait = backgroundWait;
+                if (runName != null && !runName.isBlank()) {
+                    run.name = runName;
+                }
+                List<String> args = new ArrayList<>();
+                args.add("run");
+                if (runName != null && !runName.isBlank()) {
+                    args.add("--name=" + runName);
+                }
+                args.addAll(files);
+                RunHelper.addCamelCLICommand(args);
+                ProcessBuilder pb = new ProcessBuilder();
+                pb.command(args);
+                int rc = run.runBackgroundProcess(pb, "Camel Main");
+                if (rc != 0) {
+                    return rc;
+                }
+                pid = run.spawnPid;
+                if (pid <= 0) {
+                    printer().printErr("Unable to determine the running Camel 
PID");
+                    return 1;
+                }
+                target = Long.toString(pid);
+                started = true;
+            }
+
+            if (exportPng) {
+                String hawtioUrl = "http://localhost:"; + port + "/hawtio";
+                String jolokiaUrl = "http://127.0.0.1:"; + jolokiaPort + 
"/jolokia";
+                int attachCode = attachJolokia(target, jolokiaUrl);
+                if (attachCode != 0) {
+                    return attachCode;
+                }
+                HawtioProcess hawtioProcess = startHawtioProcess(port);
+                try {
+                    waitForHawtio(hawtioUrl, hawtioProcess);
+                    exit = exportDiagramPng(hawtioUrl, jolokiaUrl);
+                } finally {
+                    stopProcess(hawtioProcess.process);
+                    if (keepRunning && jolokiaAttached) {
+                        detachJolokia(target);
+                    }
+                }
+                return exit;
+            } else {
+                Hawtio hawtio = new Hawtio(getMain());
+                List<String> hawtioArgs = new ArrayList<>();
+                if (target != null && !target.isBlank()) {
+                    hawtioArgs.add(target);
+                }
+                hawtioArgs.add("--port=" + port);
+                hawtioArgs.add("--openUrl=" + openUrl);
+                CommandLine.populateCommand(hawtio, hawtioArgs.toArray(new 
String[0]));
+                exit = hawtio.doCall();
+                return exit;
+            }
+        } finally {
+            if (started && !keepRunning) {
+                StopProcess stop = new StopProcess(getMain());
+                if (target != null && !target.isBlank()) {
+                    CommandLine.populateCommand(stop, target);
+                }
+                stop.doCall();
+            }
+        }
+    }
+
+    private int attachJolokia(String target, String jolokiaUrl) throws 
Exception {
+        if (target == null || target.isBlank()) {
+            printer().printErr("Name or PID required to attach Jolokia for PNG 
export");
+            return 1;
+        }
+        jolokiaAttached = false;
+        if (isJolokiaAvailable(jolokiaUrl)) {
+            return 0;
+        }
+        Jolokia jolokia = new Jolokia(getMain());
+        List<String> args = new ArrayList<>();
+        args.add(target);
+        args.add("--port=" + jolokiaPort);
+        CommandLine.populateCommand(jolokia, args.toArray(new String[0]));
+        int code = jolokia.doCall();
+        if (code != 0) {
+            return waitForJolokia(jolokiaUrl, 5000) ? 0 : code;
+        }
+        if (waitForJolokia(jolokiaUrl, 5000)) {
+            jolokiaAttached = true;
+            return 0;
+        }
+        printer().printErr("Unable to attach Jolokia at " + jolokiaUrl);
+        return 1;
+    }
+
+    private void detachJolokia(String target) {
+        try {
+            if (target == null || target.isBlank()) {
+                return;
+            }
+            Jolokia jolokia = new Jolokia(getMain());
+            CommandLine.populateCommand(jolokia, "--stop", target);
+            jolokia.doCall();
+        } catch (Exception e) {
+            printer().printErr("Failed to stop Jolokia: " + e.getMessage());
+        }
+    }
+
+    private HawtioProcess startHawtioProcess(int port) throws Exception {
+        List<String> args = new ArrayList<>();
+        args.add("hawtio");
+        args.add("--port=" + port);
+        args.add("--openUrl=false");
+        RunHelper.addCamelCLICommand(args);
+        ProcessBuilder pb = new ProcessBuilder();
+        pb.command(args);
+        pb.redirectErrorStream(true);
+        Process process = pb.start();
+        List<String> output = Collections.synchronizedList(new ArrayList<>());
+        startOutputPump(process.getInputStream(), output);
+        return new HawtioProcess(process, output);
+    }
+
+    private void stopProcess(Process process) {
+        if (process == null) {
+            return;
+        }
+        try {
+            process.destroy();
+            process.waitFor();
+        } catch (Exception e) {
+            // ignore
+        }
+    }
+
+    private void waitForHawtio(String hawtioUrl, HawtioProcess hawtioProcess) 
throws Exception {
+        StopWatch watch = new StopWatch();
+        while (watch.taken() < playwrightTimeout) {
+            try {
+                URL url = URI.create(hawtioUrl).toURL();
+                HttpURLConnection conn = (HttpURLConnection) 
url.openConnection();
+                conn.setConnectTimeout(1000);
+                conn.setReadTimeout(1000);
+                conn.setRequestMethod("GET");
+                int code = conn.getResponseCode();
+                if (code >= 200 && code < 500) {
+                    return;
+                }
+            } catch (Exception e) {
+                // ignore until timeout
+            }
+            if (hawtioProcess != null && !hawtioProcess.process.isAlive()) {
+                throw new IllegalStateException("Hawtio terminated before 
startup." + formatHawtioOutput(hawtioProcess.output));
+            }
+            Thread.sleep(500);

Review Comment:
   This is fragile, would it be possible to implement some kind of polling 
healthcheck instead?



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java:
##########
@@ -0,0 +1,945 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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.
+ */
+package org.apache.camel.dsl.jbang.core.commands;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Stack;
+import java.util.concurrent.ThreadLocalRandom;
+
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.BrowserType;
+import com.microsoft.playwright.Locator;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Page.NavigateOptions;
+import com.microsoft.playwright.Page.ScreenshotOptions;
+import com.microsoft.playwright.Page.WaitForFunctionOptions;
+import com.microsoft.playwright.Page.WaitForSelectorOptions;
+import com.microsoft.playwright.Playwright;
+import com.microsoft.playwright.options.ViewportSize;
+import com.microsoft.playwright.options.WaitUntilState;
+import org.apache.camel.dsl.jbang.core.commands.process.Hawtio;
+import org.apache.camel.dsl.jbang.core.commands.process.Jolokia;
+import org.apache.camel.dsl.jbang.core.commands.process.StopProcess;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.StopWatch;
+import picocli.CommandLine;
+
[email protected](name = "diagram", description = "Visualize Camel routes 
using Hawtio", sortOptions = false,
+                     showDefaultValues = true)
+public class Diagram extends CamelCommand {
+
+    @CommandLine.Parameters(description = "The Camel file(s) to run. If no 
files specified then use --name to attach to a running integration.",
+                            arity = "0..9", paramLabel = "<files>", 
parameterConsumer = FilesConsumer.class)
+    Path[] filePaths; // Defined only for file path completion; the field 
never used
+    List<String> files = new ArrayList<>();
+
+    @CommandLine.Option(names = { "--name" },
+                        description = "Name or pid of running Camel 
integration")
+    String name;
+
+    @CommandLine.Option(names = { "--renderer" },
+                        description = "Renderer to use (hawtio)",
+                        defaultValue = "hawtio")
+    String renderer = "hawtio";
+
+    @CommandLine.Option(names = { "--port" },
+                        description = "Port number to use for Hawtio web 
console (port 8888 by default)", defaultValue = "8888")
+    int port = 8888;
+
+    @CommandLine.Option(names = { "--openUrl" },
+                        description = "To automatic open Hawtio web console in 
the web browser", defaultValue = "true")
+    boolean openUrl = true;
+
+    @CommandLine.Option(names = { "--background-wait" }, defaultValue = "true",
+                        description = "To wait for run in background to 
startup successfully, before returning")
+    boolean backgroundWait = true;
+
+    @CommandLine.Option(names = { "--keep-running" }, defaultValue = "false",
+                        description = "Keep the background Camel integration 
running after exiting Hawtio")
+    boolean keepRunning;
+
+    @CommandLine.Option(names = { "--output" },
+                        description = "Write a PNG snapshot of the route 
diagram to the given file")
+    Path output;
+
+    @CommandLine.Option(names = { "--browser" },
+                        description = "Playwright browser to use (chromium 
only)",
+                        defaultValue = "chromium")
+    String browser = "chromium";
+
+    @CommandLine.Option(names = { "--playwright-browser-path" },
+                        description = "Path to the Playwright browser 
executable")
+    String playwrightBrowserPath;
+
+    @CommandLine.Option(names = { "--playwright-timeout" }, defaultValue = 
"120000",
+                        description = "Timeout in millis for Playwright 
navigation and rendering")
+    long playwrightTimeout = 120000;
+
+    @CommandLine.Option(names = { "--route-id" },
+                        description = "Route id to render (defaults to the 
first route)")
+    String routeId;
+
+    @CommandLine.Option(names = { "--jolokia-port" }, defaultValue = "8778",
+                        description = "Jolokia port to attach when exporting 
PNG")
+    int jolokiaPort = 8778;
+
+    private boolean jolokiaAttached;
+
+    public Diagram(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        String selectedRenderer = renderer == null ? "hawtio" : 
renderer.toLowerCase(Locale.ROOT);
+        if (!"hawtio".equals(selectedRenderer)) {
+            printer().printErr("Unsupported renderer: " + renderer);
+            return 1;
+        }
+
+        boolean hasFiles = files != null && !files.isEmpty();
+        boolean exportPng = output != null;
+        if (exportPng && openUrl) {
+            openUrl = false;
+        }
+
+        String runName = name;
+        if (hasFiles && (runName == null || runName.isBlank())) {
+            runName = FileUtil.onlyName(FileUtil.stripPath(files.get(0)));
+        }
+        String target = runName;
+        if (!hasFiles && (target == null || target.isBlank())) {
+            new CommandLine(this).execute("--help");
+            return 0;
+        }
+
+        long pid = 0;
+        boolean started = false;
+        int exit = 0;
+        try {
+            if (hasFiles) {
+                Run run = new Run(getMain());
+                run.backgroundWait = backgroundWait;
+                if (runName != null && !runName.isBlank()) {
+                    run.name = runName;
+                }
+                List<String> args = new ArrayList<>();
+                args.add("run");
+                if (runName != null && !runName.isBlank()) {
+                    args.add("--name=" + runName);
+                }
+                args.addAll(files);
+                RunHelper.addCamelCLICommand(args);
+                ProcessBuilder pb = new ProcessBuilder();
+                pb.command(args);
+                int rc = run.runBackgroundProcess(pb, "Camel Main");
+                if (rc != 0) {
+                    return rc;
+                }
+                pid = run.spawnPid;
+                if (pid <= 0) {
+                    printer().printErr("Unable to determine the running Camel 
PID");
+                    return 1;
+                }
+                target = Long.toString(pid);
+                started = true;
+            }
+
+            if (exportPng) {
+                String hawtioUrl = "http://localhost:"; + port + "/hawtio";
+                String jolokiaUrl = "http://127.0.0.1:"; + jolokiaPort + 
"/jolokia";
+                int attachCode = attachJolokia(target, jolokiaUrl);
+                if (attachCode != 0) {
+                    return attachCode;
+                }
+                HawtioProcess hawtioProcess = startHawtioProcess(port);
+                try {
+                    waitForHawtio(hawtioUrl, hawtioProcess);
+                    exit = exportDiagramPng(hawtioUrl, jolokiaUrl);
+                } finally {
+                    stopProcess(hawtioProcess.process);
+                    if (keepRunning && jolokiaAttached) {
+                        detachJolokia(target);
+                    }
+                }
+                return exit;
+            } else {
+                Hawtio hawtio = new Hawtio(getMain());
+                List<String> hawtioArgs = new ArrayList<>();
+                if (target != null && !target.isBlank()) {
+                    hawtioArgs.add(target);
+                }
+                hawtioArgs.add("--port=" + port);
+                hawtioArgs.add("--openUrl=" + openUrl);
+                CommandLine.populateCommand(hawtio, hawtioArgs.toArray(new 
String[0]));
+                exit = hawtio.doCall();
+                return exit;
+            }
+        } finally {
+            if (started && !keepRunning) {
+                StopProcess stop = new StopProcess(getMain());
+                if (target != null && !target.isBlank()) {
+                    CommandLine.populateCommand(stop, target);
+                }
+                stop.doCall();
+            }
+        }
+    }
+
+    private int attachJolokia(String target, String jolokiaUrl) throws 
Exception {
+        if (target == null || target.isBlank()) {
+            printer().printErr("Name or PID required to attach Jolokia for PNG 
export");
+            return 1;
+        }
+        jolokiaAttached = false;
+        if (isJolokiaAvailable(jolokiaUrl)) {
+            return 0;
+        }
+        Jolokia jolokia = new Jolokia(getMain());
+        List<String> args = new ArrayList<>();
+        args.add(target);
+        args.add("--port=" + jolokiaPort);
+        CommandLine.populateCommand(jolokia, args.toArray(new String[0]));
+        int code = jolokia.doCall();
+        if (code != 0) {
+            return waitForJolokia(jolokiaUrl, 5000) ? 0 : code;
+        }
+        if (waitForJolokia(jolokiaUrl, 5000)) {
+            jolokiaAttached = true;
+            return 0;
+        }
+        printer().printErr("Unable to attach Jolokia at " + jolokiaUrl);
+        return 1;
+    }
+
+    private void detachJolokia(String target) {
+        try {
+            if (target == null || target.isBlank()) {
+                return;
+            }
+            Jolokia jolokia = new Jolokia(getMain());
+            CommandLine.populateCommand(jolokia, "--stop", target);
+            jolokia.doCall();
+        } catch (Exception e) {
+            printer().printErr("Failed to stop Jolokia: " + e.getMessage());
+        }
+    }
+
+    private HawtioProcess startHawtioProcess(int port) throws Exception {
+        List<String> args = new ArrayList<>();
+        args.add("hawtio");
+        args.add("--port=" + port);
+        args.add("--openUrl=false");
+        RunHelper.addCamelCLICommand(args);
+        ProcessBuilder pb = new ProcessBuilder();
+        pb.command(args);
+        pb.redirectErrorStream(true);
+        Process process = pb.start();
+        List<String> output = Collections.synchronizedList(new ArrayList<>());
+        startOutputPump(process.getInputStream(), output);
+        return new HawtioProcess(process, output);
+    }
+
+    private void stopProcess(Process process) {
+        if (process == null) {
+            return;
+        }
+        try {
+            process.destroy();
+            process.waitFor();
+        } catch (Exception e) {
+            // ignore
+        }
+    }
+
+    private void waitForHawtio(String hawtioUrl, HawtioProcess hawtioProcess) 
throws Exception {
+        StopWatch watch = new StopWatch();
+        while (watch.taken() < playwrightTimeout) {
+            try {
+                URL url = URI.create(hawtioUrl).toURL();
+                HttpURLConnection conn = (HttpURLConnection) 
url.openConnection();
+                conn.setConnectTimeout(1000);
+                conn.setReadTimeout(1000);
+                conn.setRequestMethod("GET");
+                int code = conn.getResponseCode();
+                if (code >= 200 && code < 500) {
+                    return;
+                }
+            } catch (Exception e) {
+                // ignore until timeout
+            }
+            if (hawtioProcess != null && !hawtioProcess.process.isAlive()) {
+                throw new IllegalStateException("Hawtio terminated before 
startup." + formatHawtioOutput(hawtioProcess.output));
+            }
+            Thread.sleep(500);
+        }
+        throw new IllegalStateException(
+                "Hawtio did not start within " + playwrightTimeout + " ms." + 
formatHawtioOutput(hawtioProcess.output));
+    }
+
+    private boolean isJolokiaAvailable(String jolokiaUrl) {
+        try {
+            String probeUrl = jolokiaUrl.endsWith("/") ? jolokiaUrl + 
"version" : jolokiaUrl + "/version";
+            URL url = URI.create(probeUrl).toURL();
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setConnectTimeout(1000);
+            conn.setReadTimeout(1000);
+            conn.setRequestMethod("GET");
+            int code = conn.getResponseCode();
+            return code >= 200 && code < 500 && code != 404;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private boolean waitForJolokia(String jolokiaUrl, long timeoutMs) {
+        StopWatch watch = new StopWatch();
+        while (watch.taken() < timeoutMs) {
+            if (isJolokiaAvailable(jolokiaUrl)) {
+                return true;
+            }
+            try {
+                Thread.sleep(200);

Review Comment:
   same



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to