| @@ -1,13 +1,13 | |||||
| 1 | package org.implab.gradle.common.dsl; |
|
1 | package org.implab.gradle.common.dsl; | |
| 2 |
|
2 | |||
| 3 |
|
3 | |||
| 4 | import org.gradle.api.provider.ListProperty; |
|
4 | import org.gradle.api.provider.ListProperty; | |
| 5 |
|
5 | |||
| 6 | public interface CommandSpec { |
|
6 | public interface TaskCommandSpecMixin { | |
| 7 | ListProperty<String> getCommandLine(); |
|
7 | ListProperty<String> getCommandLine(); | |
| 8 |
|
8 | |||
| 9 | void commandLine(Object arg0, Object... args); |
|
9 | void commandLine(Object arg0, Object... args); | |
| 10 |
|
10 | |||
| 11 | void args(Object arg0, Object... args); |
|
11 | void args(Object arg0, Object... args); | |
| 12 |
|
12 | |||
| 13 | } |
|
13 | } | |
| @@ -1,48 +1,52 | |||||
| 1 | package org.implab.gradle.common.dsl; |
|
1 | package org.implab.gradle.common.dsl; | |
| 2 |
|
2 | |||
| 3 | import java.util.HashMap; |
|
3 | import java.util.HashMap; | |
| 4 | import java.util.Map; |
|
4 | import java.util.Map; | |
| 5 |
|
5 | |||
| 6 | import org.gradle.api.Action; |
|
6 | import org.gradle.api.Action; | |
| 7 | import org.gradle.api.file.DirectoryProperty; |
|
7 | import org.gradle.api.file.DirectoryProperty; | |
| 8 | import org.gradle.api.provider.MapProperty; |
|
8 | import org.gradle.api.provider.MapProperty; | |
|
|
9 | import org.gradle.api.provider.Property; | |||
| 9 | import org.implab.gradle.common.utils.Closures; |
|
10 | import org.implab.gradle.common.utils.Closures; | |
| 10 | import org.implab.gradle.common.utils.Values; |
|
11 | import org.implab.gradle.common.utils.Values; | |
| 11 |
|
12 | |||
| 12 | import groovy.lang.Closure; |
|
13 | import groovy.lang.Closure; | |
| 13 |
|
14 | |||
| 14 | /** |
|
15 | /** | |
| 15 | * Configuration properties of the execution shell. This object specifies a |
|
16 | * Configuration properties of the execution shell. This object specifies a | |
| 16 | * working directory and environment variables for the processes started within |
|
17 | * working directory and environment variables for the processes started within | |
| 17 | * this shell. |
|
18 | * this shell. | |
| 18 | */ |
|
19 | */ | |
| 19 |
public interface Env |
|
20 | public interface TaskEnvSpecMixin { | |
|
|
21 | ||||
|
|
22 | /** Inherit environment from current process */ | |||
|
|
23 | Property<Boolean> getInheritEnvironment(); | |||
| 20 |
|
24 | |||
| 21 | /** Working directory */ |
|
25 | /** Working directory */ | |
| 22 | DirectoryProperty getWorkingDirectory(); |
|
26 | DirectoryProperty getWorkingDirectory(); | |
| 23 |
|
27 | |||
| 24 | /** Environment variables */ |
|
28 | /** Environment variables */ | |
| 25 | MapProperty<String, String> getEnvironment(); |
|
29 | MapProperty<String, String> getEnvironment(); | |
| 26 |
|
30 | |||
| 27 | /** |
|
31 | /** | |
| 28 | * Configures the environment variable using the specified action. The |
|
32 | * Configures the environment variable using the specified action. The | |
| 29 | * action is called when the value is calculated; |
|
33 | * action is called when the value is calculated; | |
| 30 | */ |
|
34 | */ | |
| 31 | default void env(Action<Map<String, Object>> configure) { |
|
35 | default void env(Action<Map<String, Object>> configure) { | |
| 32 | var provider = getEnvironment() |
|
36 | var provider = getEnvironment() | |
| 33 | .orElse(Map.of()) |
|
37 | .orElse(Map.of()) | |
| 34 | .map((base) -> { |
|
38 | .map((base) -> { | |
| 35 | var props = new HashMap<String, Object>(base); |
|
39 | var props = new HashMap<String, Object>(base); | |
| 36 |
|
40 | |||
| 37 | configure.execute(props); |
|
41 | configure.execute(props); | |
| 38 |
|
42 | |||
| 39 | return Values.mapValues(props, Values::toString); |
|
43 | return Values.mapValues(props, Values::toString); | |
| 40 | }); |
|
44 | }); | |
| 41 |
|
45 | |||
| 42 | getEnvironment().set(provider); |
|
46 | getEnvironment().set(provider); | |
| 43 | } |
|
47 | } | |
| 44 |
|
48 | |||
| 45 | default void env(Closure<?> configure) { |
|
49 | default void env(Closure<?> configure) { | |
| 46 | env(Closures.action(configure)); |
|
50 | env(Closures.action(configure)); | |
| 47 | } |
|
51 | } | |
| 48 | } |
|
52 | } | |
| @@ -1,12 +1,12 | |||||
| 1 | package org.implab.gradle.common.dsl; |
|
1 | package org.implab.gradle.common.dsl; | |
| 2 |
|
2 | |||
| 3 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
3 | import org.eclipse.jdt.annotation.NonNullByDefault; | |
| 4 |
|
4 | |||
| 5 | @NonNullByDefault |
|
5 | @NonNullByDefault | |
| 6 | public interface PipeSpec { |
|
6 | public interface TaskPipeSpecMixin { | |
| 7 | RedirectToSpec getStdout(); |
|
7 | RedirectToSpec getStdout(); | |
| 8 |
|
8 | |||
| 9 | RedirectToSpec getStderr(); |
|
9 | RedirectToSpec getStderr(); | |
| 10 |
|
10 | |||
| 11 | RedirectFromSpec getStdin(); |
|
11 | RedirectFromSpec getStdin(); | |
| 12 | } |
|
12 | } | |
| @@ -1,110 +1,115 | |||||
| 1 | package org.implab.gradle.common.tasks; |
|
1 | package org.implab.gradle.common.tasks; | |
| 2 |
|
2 | |||
| 3 | import java.io.IOException; |
|
3 | import java.io.IOException; | |
| 4 | import java.util.Map; |
|
4 | import java.util.Map; | |
| 5 | import java.util.concurrent.ExecutionException; |
|
5 | import java.util.concurrent.ExecutionException; | |
| 6 | import java.util.stream.Stream; |
|
6 | import java.util.stream.Stream; | |
| 7 |
|
7 | |||
| 8 | import org.gradle.api.DefaultTask; |
|
8 | import org.gradle.api.DefaultTask; | |
| 9 | import org.gradle.api.file.DirectoryProperty; |
|
9 | import org.gradle.api.file.DirectoryProperty; | |
| 10 | import org.gradle.api.provider.ListProperty; |
|
10 | import org.gradle.api.provider.ListProperty; | |
| 11 | import org.gradle.api.provider.MapProperty; |
|
11 | import org.gradle.api.provider.MapProperty; | |
|
|
12 | import org.gradle.api.provider.Property; | |||
| 12 | import org.gradle.api.tasks.Internal; |
|
13 | import org.gradle.api.tasks.Internal; | |
| 13 | import org.gradle.api.tasks.TaskAction; |
|
14 | import org.gradle.api.tasks.TaskAction; | |
| 14 | import org.implab.gradle.common.dsl.CommandSpec; |
|
15 | import org.implab.gradle.common.dsl.TaskCommandSpecMixin; | |
| 15 | import org.implab.gradle.common.dsl.PipeSpec; |
|
16 | import org.implab.gradle.common.dsl.TaskPipeSpecMixin; | |
| 16 | import org.implab.gradle.common.dsl.RedirectFromSpec; |
|
17 | import org.implab.gradle.common.dsl.RedirectFromSpec; | |
| 17 | import org.implab.gradle.common.dsl.RedirectToSpec; |
|
18 | import org.implab.gradle.common.dsl.RedirectToSpec; | |
| 18 |
import org.implab.gradle.common.dsl.Env |
|
19 | import org.implab.gradle.common.dsl.TaskEnvSpecMixin; | |
| 19 | import org.implab.gradle.common.exec.ExecBuilder; |
|
20 | import org.implab.gradle.common.exec.ExecBuilder; | |
| 20 | import org.implab.gradle.common.utils.ObjectsMixin; |
|
21 | import org.implab.gradle.common.utils.ObjectsMixin; | |
| 21 | import org.implab.gradle.common.utils.Strings; |
|
22 | import org.implab.gradle.common.utils.Strings; | |
| 22 | import org.implab.gradle.common.utils.ThrowingConsumer; |
|
23 | import org.implab.gradle.common.utils.ThrowingConsumer; | |
| 23 |
|
24 | |||
| 24 | public abstract class ShellExecTask |
|
25 | public abstract class ShellExecTask | |
| 25 | extends DefaultTask |
|
26 | extends DefaultTask | |
| 26 |
implements CommandSpec, PipeSpec, Env |
|
27 | implements TaskCommandSpecMixin, TaskPipeSpecMixin, TaskEnvSpecMixin, ObjectsMixin { | |
| 27 |
|
28 | |||
| 28 | private final RedirectToSpec redirectStderr = new RedirectToSpec(); |
|
29 | private final RedirectToSpec redirectStderr = new RedirectToSpec(); | |
| 29 |
|
30 | |||
| 30 | private final RedirectToSpec redirectStdout = new RedirectToSpec(); |
|
31 | private final RedirectToSpec redirectStdout = new RedirectToSpec(); | |
| 31 |
|
32 | |||
| 32 | private final RedirectFromSpec redirectStdin = new RedirectFromSpec(); |
|
33 | private final RedirectFromSpec redirectStdin = new RedirectFromSpec(); | |
| 33 |
|
34 | |||
| 34 | @Internal |
|
35 | @Internal | |
| 35 | @Override |
|
36 | @Override | |
|
|
37 | public abstract Property<Boolean> getInheritEnvironment(); | |||
|
|
38 | ||||
|
|
39 | @Internal | |||
|
|
40 | @Override | |||
| 36 | public abstract DirectoryProperty getWorkingDirectory(); |
|
41 | public abstract DirectoryProperty getWorkingDirectory(); | |
| 37 |
|
42 | |||
| 38 | @Internal |
|
43 | @Internal | |
| 39 | @Override |
|
44 | @Override | |
| 40 | public abstract MapProperty<String, String> getEnvironment(); |
|
45 | public abstract MapProperty<String, String> getEnvironment(); | |
| 41 |
|
46 | |||
| 42 | @Internal |
|
47 | @Internal | |
| 43 | @Override |
|
48 | @Override | |
| 44 | public abstract ListProperty<String> getCommandLine(); |
|
49 | public abstract ListProperty<String> getCommandLine(); | |
| 45 |
|
50 | |||
| 46 | /** |
|
51 | /** | |
| 47 | * STDIN redirection, if not specified, no input will be passed to the command |
|
52 | * STDIN redirection, if not specified, no input will be passed to the command | |
| 48 | */ |
|
53 | */ | |
| 49 | @Internal |
|
54 | @Internal | |
| 50 | @Override |
|
55 | @Override | |
| 51 | public RedirectFromSpec getStdin() { |
|
56 | public RedirectFromSpec getStdin() { | |
| 52 | return redirectStdin; |
|
57 | return redirectStdin; | |
| 53 | } |
|
58 | } | |
| 54 |
|
59 | |||
| 55 | /** |
|
60 | /** | |
| 56 | * STDOUT redirection, if not specified, redirected to logger::info |
|
61 | * STDOUT redirection, if not specified, redirected to logger::info | |
| 57 | */ |
|
62 | */ | |
| 58 | @Internal |
|
63 | @Internal | |
| 59 | @Override |
|
64 | @Override | |
| 60 | public RedirectToSpec getStdout() { |
|
65 | public RedirectToSpec getStdout() { | |
| 61 | return redirectStdout; |
|
66 | return redirectStdout; | |
| 62 | } |
|
67 | } | |
| 63 |
|
68 | |||
| 64 | /** |
|
69 | /** | |
| 65 | * STDERR redirection, if not specified, redirected to logger::error |
|
70 | * STDERR redirection, if not specified, redirected to logger::error | |
| 66 | */ |
|
71 | */ | |
| 67 | @Internal |
|
72 | @Internal | |
| 68 | @Override |
|
73 | @Override | |
| 69 | public RedirectToSpec getStderr() { |
|
74 | public RedirectToSpec getStderr() { | |
| 70 | return redirectStderr; |
|
75 | return redirectStderr; | |
| 71 | } |
|
76 | } | |
| 72 |
|
77 | |||
| 73 | @Override |
|
78 | @Override | |
| 74 | public void commandLine(Object arg0, Object... args) { |
|
79 | public void commandLine(Object arg0, Object... args) { | |
| 75 | getCommandLine().set(provider(() -> Stream.concat( |
|
80 | getCommandLine().set(provider(() -> Stream.concat( | |
| 76 | Stream.of(arg0), |
|
81 | Stream.of(arg0), | |
| 77 | Stream.of(args)) |
|
82 | Stream.of(args)) | |
| 78 | .map(Strings::asString).toList())); |
|
83 | .map(Strings::asString).toList())); | |
| 79 |
|
84 | |||
| 80 | } |
|
85 | } | |
| 81 |
|
86 | |||
| 82 | @Override |
|
87 | @Override | |
| 83 | public void args(Object arg0, Object... args) { |
|
88 | public void args(Object arg0, Object... args) { | |
| 84 | getCommandLine().addAll(provider(() -> Stream.concat( |
|
89 | getCommandLine().addAll(provider(() -> Stream.concat( | |
| 85 | Stream.of(arg0), |
|
90 | Stream.of(arg0), | |
| 86 | Stream.of(args)) |
|
91 | Stream.of(args)) | |
| 87 | .map(Strings::asString).toList())); |
|
92 | .map(Strings::asString).toList())); | |
| 88 | } |
|
93 | } | |
| 89 |
|
94 | |||
| 90 | protected abstract ExecBuilder execBuilder(); |
|
95 | protected abstract ExecBuilder execBuilder(); | |
| 91 |
|
96 | |||
| 92 | @TaskAction |
|
97 | @TaskAction | |
| 93 | public final void run() throws IOException, InterruptedException, ExecutionException { |
|
98 | public final void run() throws IOException, InterruptedException, ExecutionException { | |
| 94 | var execBuilder = execBuilder(); |
|
99 | var execBuilder = execBuilder(); | |
| 95 | execBuilder.workingDirectory(getWorkingDirectory().get().getAsFile()); |
|
100 | execBuilder.workingDirectory(getWorkingDirectory().get().getAsFile()); | |
| 96 | execBuilder.environment(getEnvironment().getOrElse(Map.of())); |
|
101 | execBuilder.environment(getEnvironment().getOrElse(Map.of())); | |
| 97 | execBuilder.commandLine(getCommandLine().get()); |
|
102 | execBuilder.commandLine(getCommandLine().get()); | |
| 98 |
|
103 | |||
| 99 | getStdout().getRedirection().ifPresent(execBuilder::stdout); |
|
104 | getStdout().getRedirection().ifPresent(execBuilder::stdout); | |
| 100 | getStderr().getRedirection().ifPresent(execBuilder::stderr); |
|
105 | getStderr().getRedirection().ifPresent(execBuilder::stderr); | |
| 101 | getStdin().getRedirection().ifPresent(execBuilder::stdin); |
|
106 | getStdin().getRedirection().ifPresent(execBuilder::stdin); | |
| 102 |
|
107 | |||
| 103 | execBuilder.exec().thenAccept(ThrowingConsumer.guard(this::checkRetCode)).join(); |
|
108 | execBuilder.exec().thenAccept(ThrowingConsumer.guard(this::checkRetCode)).join(); | |
| 104 | } |
|
109 | } | |
| 105 |
|
110 | |||
| 106 | protected void checkRetCode(Integer code) throws IOException { |
|
111 | protected void checkRetCode(Integer code) throws IOException { | |
| 107 | throw new IOException(String.format("The process is terminated with code %s", code)); |
|
112 | throw new IOException(String.format("The process is terminated with code %s", code)); | |
| 108 | } |
|
113 | } | |
| 109 |
|
114 | |||
| 110 | } |
|
115 | } | |
| @@ -1,24 +1,21 | |||||
| 1 | package org.implab.gradle.common.utils; |
|
1 | package org.implab.gradle.common.utils; | |
| 2 |
|
2 | |||
| 3 | import groovy.lang.Closure; |
|
3 | import groovy.lang.Closure; | |
| 4 |
|
4 | |||
| 5 | import org.eclipse.jdt.annotation.NonNullByDefault; |
|
5 | import org.eclipse.jdt.annotation.NonNullByDefault; | |
| 6 | import org.gradle.api.Action; |
|
6 | import org.gradle.api.Action; | |
| 7 |
|
7 | |||
| 8 | @NonNullByDefault |
|
8 | @NonNullByDefault | |
| 9 | public final class Closures { |
|
9 | public final class Closures { | |
| 10 | private Closures() { |
|
10 | private Closures() { | |
| 11 | } |
|
11 | } | |
| 12 |
|
12 | |||
| 13 | public static <T> Action<T> action(Closure<?> closure) { |
|
13 | public static <T> Action<T> action(Closure<?> closure) { | |
| 14 |
return arg -> |
|
14 | return arg -> apply(closure, arg); | |
| 15 | closure.setDelegate(arg); |
|
|||
| 16 | closure.call(arg); |
|
|||
| 17 | }; |
|
|||
| 18 | } |
|
15 | } | |
| 19 |
|
16 | |||
| 20 | public static void apply(Closure<?> action, Object target) { |
|
17 | public static void apply(Closure<?> action, Object target) { | |
| 21 | action.setDelegate(target); |
|
18 | action.setDelegate(target); | |
| 22 | action.call(target); |
|
19 | action.call(target); | |
| 23 | } |
|
20 | } | |
| 24 | } |
|
21 | } | |
| @@ -1,89 +1,93 | |||||
| 1 | package org.implab.gradle.common.utils; |
|
1 | package org.implab.gradle.common.utils; | |
| 2 |
|
2 | |||
| 3 | import java.util.Iterator; |
|
3 | import java.util.Iterator; | |
| 4 | import java.util.Map; |
|
4 | import java.util.Map; | |
| 5 | import java.util.Spliterators; |
|
5 | import java.util.Spliterators; | |
| 6 | import java.util.Map.Entry; |
|
6 | import java.util.Map.Entry; | |
| 7 | import java.util.Optional; |
|
7 | import java.util.Optional; | |
| 8 | import java.util.function.Function; |
|
8 | import java.util.function.Function; | |
| 9 | import java.util.function.Supplier; |
|
9 | import java.util.function.Supplier; | |
| 10 | import java.util.stream.Collectors; |
|
10 | import java.util.stream.Collectors; | |
| 11 | import java.util.stream.Stream; |
|
11 | import java.util.stream.Stream; | |
| 12 | import java.util.stream.StreamSupport; |
|
12 | import java.util.stream.StreamSupport; | |
| 13 |
|
13 | |||
| 14 | import org.gradle.api.provider.Provider; |
|
14 | import org.gradle.api.provider.Provider; | |
| 15 |
|
15 | |||
| 16 | public final class Values { |
|
16 | public final class Values { | |
| 17 |
|
17 | |||
| 18 | private Values() { |
|
18 | private Values() { | |
| 19 | } |
|
19 | } | |
| 20 |
|
20 | |||
| 21 | /** |
|
21 | /** | |
| 22 | * Converts values in the specified map |
|
22 | * Converts values in the specified map | |
| 23 | * |
|
23 | * | |
| 24 | * @param <K> |
|
24 | * @param <K> | |
| 25 | * @param <V> |
|
25 | * @param <V> | |
| 26 | * @param <U> |
|
26 | * @param <U> | |
| 27 | * @param map |
|
27 | * @param map | |
| 28 | * @param mapper |
|
28 | * @param mapper | |
| 29 | * @return |
|
29 | * @return | |
| 30 | */ |
|
30 | */ | |
| 31 | public static <K, V, U> Map<K, U> mapValues(Map<K, V> map, Function<V, U> mapper) { |
|
31 | public static <K, V, U> Map<K, U> mapValues(Map<K, V> map, Function<V, U> mapper) { | |
| 32 | Function<Entry<K, V>, V> getter = Entry::getValue; |
|
32 | Function<Entry<K, V>, V> getter = Entry::getValue; | |
| 33 |
|
33 | |||
| 34 | return map.entrySet().stream() |
|
34 | return map.entrySet().stream() | |
| 35 | .collect(Collectors.toMap(Entry::getKey, getter.andThen(mapper))); |
|
35 | .collect(Collectors.toMap(Entry::getKey, getter.andThen(mapper))); | |
| 36 | } |
|
36 | } | |
| 37 |
|
37 | |||
| 38 | /** |
|
38 | /** | |
| 39 | * Converts the supplied value to a string. |
|
39 | * Converts the supplied value to a string. | |
| 40 | */ |
|
40 | */ | |
| 41 | public static String toString(Object value) { |
|
41 | public static String toString(Object value) { | |
| 42 | if (value == null) { |
|
42 | if (value == null) { | |
| 43 | return null; |
|
43 | return null; | |
| 44 | } else if (value instanceof String string) { |
|
44 | } else if (value instanceof String string) { | |
| 45 | return string; |
|
45 | return string; | |
| 46 | } else if (value instanceof Provider<?> provider) { |
|
46 | } else if (value instanceof Provider<?> provider) { | |
| 47 | return toString(provider.getOrNull()); |
|
47 | return toString(provider.getOrNull()); | |
| 48 | } else if (value instanceof Supplier<?> supplier) { |
|
48 | } else if (value instanceof Supplier<?> supplier) { | |
| 49 | return toString(supplier.get()); |
|
49 | return toString(supplier.get()); | |
| 50 | } else { |
|
50 | } else { | |
| 51 | return value.toString(); |
|
51 | return value.toString(); | |
| 52 | } |
|
52 | } | |
| 53 | } |
|
53 | } | |
| 54 |
|
54 | |||
| 55 | public static <T> Stream<T> stream(Iterator<T> remaining) { |
|
55 | public static <T> Stream<T> stream(Iterator<T> remaining) { | |
| 56 | return StreamSupport.stream( |
|
56 | return StreamSupport.stream( | |
| 57 | Spliterators.spliteratorUnknownSize(remaining, 0), |
|
57 | Spliterators.spliteratorUnknownSize(remaining, 0), | |
| 58 | false); |
|
58 | false); | |
| 59 | } |
|
59 | } | |
| 60 |
|
60 | |||
| 61 | public static <T> Optional<T> take(Iterator<T> iterator) { |
|
61 | public static <T> Optional<T> take(Iterator<T> iterator) { | |
| 62 | return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty(); |
|
62 | return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty(); | |
| 63 | } |
|
63 | } | |
| 64 |
|
64 | |||
| 65 | public static <T> Iterable<T> iterable(T[] values) { |
|
65 | public static <T> Iterable<T> iterable(T[] values) { | |
| 66 | return () -> new ArrayIterator<>(values); |
|
66 | return () -> new ArrayIterator<>(values); | |
| 67 | } |
|
67 | } | |
| 68 |
|
68 | |||
|
|
69 | public static <T> Optional<T> optional(Provider<T> provider) { | |||
|
|
70 | return provider.isPresent() ? Optional.of(provider.get()) : Optional.empty(); | |||
|
|
71 | } | |||
|
|
72 | ||||
| 69 | private static class ArrayIterator<T> implements Iterator<T> { |
|
73 | private static class ArrayIterator<T> implements Iterator<T> { | |
| 70 | private final T[] data; |
|
74 | private final T[] data; | |
| 71 |
|
75 | |||
| 72 | private int pos = 0; |
|
76 | private int pos = 0; | |
| 73 |
|
77 | |||
| 74 | ArrayIterator(T[] data) { |
|
78 | ArrayIterator(T[] data) { | |
| 75 | this.data = data; |
|
79 | this.data = data; | |
| 76 | } |
|
80 | } | |
| 77 |
|
81 | |||
| 78 | @Override |
|
82 | @Override | |
| 79 | public boolean hasNext() { |
|
83 | public boolean hasNext() { | |
| 80 | return pos < data.length; |
|
84 | return pos < data.length; | |
| 81 | } |
|
85 | } | |
| 82 |
|
86 | |||
| 83 | @Override |
|
87 | @Override | |
| 84 | public T next() { |
|
88 | public T next() { | |
| 85 | return data[pos++]; |
|
89 | return data[pos++]; | |
| 86 | } |
|
90 | } | |
| 87 | } |
|
91 | } | |
| 88 |
|
92 | |||
| 89 | } |
|
93 | } | |
| 1 | NO CONTENT: file was removed |
|
NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now
