GraphExporter.java revision 227b47625d7482b5b47ad0e4c70ce0a246236ade
1227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks/* 2227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * Copyright (C) 2012 The Android Open Source Project 3227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * 4227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * Licensed under the Apache License, Version 2.0 (the "License"); 5227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * you may not use this file except in compliance with the License. 6227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * You may obtain a copy of the License at 7227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * 8227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * http://www.apache.org/licenses/LICENSE-2.0 9227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * 10227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * Unless required by applicable law or agreed to in writing, software 11227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * distributed under the License is distributed on an "AS IS" BASIS, 12227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * See the License for the specific language governing permissions and 14227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * limitations under the License. 15227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks */ 16227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 17227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks// This class provides functions to export a FilterGraph. 18227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 19227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendrickspackage androidx.media.filterfw; 20227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 21227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricksimport android.content.Context; 22227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 23227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricksimport java.io.FileOutputStream; 24227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricksimport java.io.OutputStreamWriter; 25227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricksimport java.util.HashMap; 26227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricksimport java.util.HashSet; 27227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricksimport java.util.Map.Entry; 28227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricksimport java.util.Set; 29227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 30227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks/** 31227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * This class provides functions to export a FilterGraph as a DOT file. 32227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks */ 33227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendrickspublic class GraphExporter { 34227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 35227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks /** 36227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * Exports the graph as DOT (see http://en.wikipedia.org/wiki/DOT_language). 37227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * Using the exported file, the graph can be visualized e.g. with the command line tool dot. 38227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * Optionally, one may /exclude/ unconnected optional ports (third parameter = false), 39227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * since they can quickly clutter the visualization (and, depending on the purpose, may not 40227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * be interesting). 41227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * 42227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * Example workflow: 43227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * 1. run application on device, make sure it calls exportGraphAsDOT(...); 44227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * 2. adb pull /data/data/<application name>/files/<graph filename>.gv graph.gv 45227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * 3. dot -Tpng graph.gv -o graph.png 46227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks * 4. eog graph.png 47227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks */ 48227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks static public void exportAsDot(FilterGraph graph, String filename, 49227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks boolean includeUnconnectedOptionalPorts) 50227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks throws java.io.FileNotFoundException, java.io.IOException { 51227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Initialize, open file stream 52227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks Context myAppContext = graph.getContext().getApplicationContext(); 53227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks Filter[] filters = graph.getAllFilters(); 54227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks FileOutputStream fOut = myAppContext.openFileOutput(filename, Context.MODE_PRIVATE); 55227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks OutputStreamWriter dotFile = new OutputStreamWriter(fOut); 56227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 57227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Write beginning of DOT file 58227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write("digraph graphname {\n"); 59227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(" node [shape=record];\n"); 60227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 61227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // N.B. For specification and lots of examples of the DOT language, see 62227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // http://www.graphviz.org/Documentation/dotguide.pdf 63227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 64227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Iterate over all filters of the graph, write corresponding DOT node elements 65227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 66227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks for(Filter filter : filters) { 67227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(getDotName(" " + filter.getName()) + " [label=\"{"); 68227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 69227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Write upper part of element (i.e., input ports) 70227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks Set<String> inputPorts = getInputPorts(filter, includeUnconnectedOptionalPorts); 71227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(inputPorts.size() > 0) { 72227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(" { "); 73227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks int counter = 0; 74227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks for(String p : inputPorts) { 75227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write("<" + getDotName(p) + "_IN>" + p); 76227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(++counter != inputPorts.size()) dotFile.write(" | "); 77227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 78227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(" } | "); 79227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 80227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 81227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Write center part of element (i.e., element label) 82227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(filter.getName()); 83227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 84227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Write lower part of element (i.e., output ports) 85227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks Set<String> outputPorts = getOutputPorts(filter, includeUnconnectedOptionalPorts); 86227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(outputPorts.size() > 0) { 87227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(" | { "); 88227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks int counter = 0; 89227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks for(String p : outputPorts) { 90227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write("<" + getDotName(p) + "_OUT>" + p); 91227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(++counter != outputPorts.size()) dotFile.write(" | "); 92227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 93227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(" } "); 94227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 95227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 96227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write("}\"];\n"); 97227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 98227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write("\n"); 99227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 100227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Iterate over all filters again to collect connections and find unconnected ports 101227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 102227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks int dummyNodeCounter = 0; 103227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks for(Filter filter : filters) { 104227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks Set<String> outputPorts = getOutputPorts(filter, includeUnconnectedOptionalPorts); 105227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks for(String portName : outputPorts) { 106227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks OutputPort source = filter.getConnectedOutputPort(portName); 107227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(source != null) { 108227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Found a connection, draw it 109227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks InputPort target = source.getTarget(); 110227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(" " + 111227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks getDotName(source.getFilter().getName()) + ":" + 112227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks getDotName(source.getName()) + "_OUT -> " + 113227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks getDotName(target.getFilter().getName()) + ":" + 114227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks getDotName(target.getName()) + "_IN;\n" ); 115227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } else { 116227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Found a unconnected output port, add dummy node 117227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks String color = filter.getSignature().getOutputPortInfo(portName).isRequired() 118227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks ? "red" : "blue"; // red for unconnected, required ports 119227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(" " + 120227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks "dummy" + (++dummyNodeCounter) + 121227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks " [shape=point,label=\"\",color=" + color + "];\n" + 122227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks " " + getDotName(filter.getName()) + ":" + 123227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks getDotName(portName) + "_OUT -> " + 124227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks "dummy" + dummyNodeCounter + " [color=" + color + "];\n"); 125227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 126227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 127227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 128227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks Set<String> inputPorts = getInputPorts(filter, includeUnconnectedOptionalPorts); 129227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks for(String portName : inputPorts) { 130227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks InputPort target = filter.getConnectedInputPort(portName); 131227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(target != null) { 132227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Found a connection -- nothing to do, connections have been written out above 133227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } else { 134227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Found a unconnected input port, add dummy node 135227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks String color = filter.getSignature().getInputPortInfo(portName).isRequired() 136227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks ? "red" : "blue"; // red for unconnected, required ports 137227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write(" " + 138227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks "dummy" + (++dummyNodeCounter) + 139227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks " [shape=point,label=\"\",color=" + color + "];\n" + 140227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks " dummy" + dummyNodeCounter + " -> " + 141227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks getDotName(filter.getName()) + ":" + 142227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks getDotName(portName) + "_IN [color=" + color + "];\n"); 143227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 144227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 145227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 146227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 147227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Write end of DOT file, close file stream 148227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.write("}\n"); 149227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.flush(); 150227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks dotFile.close(); 151227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 152227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 153227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Internal methods 154227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 155227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // From element's name in XML, create DOT-allowed element name 156227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks static private String getDotName(String raw) { 157227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks return raw.replaceAll("\\.", "___"); // DOT does not allow . in element names 158227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 159227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 160227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Retrieve all input ports of a filter, including: 161227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // unconnected ports (which can not be retrieved from the filter, only from the signature), and 162227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // additional (connected) ports not listed in the signature (which is allowed by default, 163227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // unless disallowOtherInputs is defined in signature). 164227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // With second parameter = false, *omit* unconnected optional ports. 165227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks static private Set<String> getInputPorts(Filter filter, boolean includeUnconnectedOptional) { 166227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // add (connected) ports from filter 167227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks Set<String> ports = new HashSet<String>(); 168227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks ports.addAll(filter.getConnectedInputPortMap().keySet()); 169227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 170227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // add (unconnected) ports from signature 171227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks HashMap<String, Signature.PortInfo> signaturePorts = filter.getSignature().getInputPorts(); 172227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(signaturePorts != null){ 173227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks for(Entry<String, Signature.PortInfo> e : signaturePorts.entrySet()) { 174227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(includeUnconnectedOptional || e.getValue().isRequired()) { 175227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks ports.add(e.getKey()); 176227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 177227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 178227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 179227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks return ports; 180227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 181227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 182227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // Retrieve all output ports of a filter (analogous to above function) 183227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks static private Set<String> getOutputPorts(Filter filter, boolean includeUnconnectedOptional) { 184227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // add (connected) ports from filter 185227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks Set<String> ports = new HashSet<String>(); 186227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks ports.addAll(filter.getConnectedOutputPortMap().keySet()); 187227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks 188227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks // add (unconnected) ports from signature 189227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks HashMap<String, Signature.PortInfo> signaturePorts = filter.getSignature().getOutputPorts(); 190227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(signaturePorts != null){ 191227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks for(Entry<String, Signature.PortInfo> e : signaturePorts.entrySet()) { 192227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks if(includeUnconnectedOptional || e.getValue().isRequired()) { 193227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks ports.add(e.getKey()); 194227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 195227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 196227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 197227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks return ports; 198227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks } 199227b47625d7482b5b47ad0e4c70ce0a246236adeBenjamin Hendricks} 200