From fc2e3d2a44bd084b15d45f6a9afa9bd033abfd65 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Tue, 15 May 2018 16:39:57 +0200 Subject: [PATCH] Monitoring perspective --- org.argeo.cms.e4/e4xmi/cms-devops.e4xmi | 34 +++- .../argeo/cms/e4/monitoring/BundleNode.java | 46 +++++ .../argeo/cms/e4/monitoring/BundlesView.java | 129 ++++++++++++ .../cms/e4/monitoring/CmsSessionsView.java | 189 ++++++++++++++++++ .../argeo/cms/e4/monitoring/ModulesView.java | 106 ++++++++++ .../cms/e4/monitoring/OsgiExplorerImages.java | 29 +++ .../e4/monitoring/ServiceReferenceNode.java | 46 +++++ .../cms/e4/monitoring/StateLabelProvider.java | 82 ++++++++ org.argeo.cms.ui.theme/icons/logout.png | Bin 0 -> 328 bytes 9 files changed, 651 insertions(+), 10 deletions(-) create mode 100644 org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java create mode 100644 org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java create mode 100644 org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java create mode 100644 org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java create mode 100644 org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java create mode 100644 org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java create mode 100644 org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java create mode 100644 org.argeo.cms.ui.theme/icons/logout.png diff --git a/org.argeo.cms.e4/e4xmi/cms-devops.e4xmi b/org.argeo.cms.e4/e4xmi/cms-devops.e4xmi index 44dfed840..c56bbab46 100644 --- a/org.argeo.cms.e4/e4xmi/cms-devops.e4xmi +++ b/org.argeo.cms.e4/e4xmi/cms-devops.e4xmi @@ -3,7 +3,7 @@ shellMaximized - + @@ -29,14 +29,6 @@ - - - - - - - - @@ -64,6 +56,21 @@ + + + + + + + + + + + + + + + @@ -74,9 +81,14 @@ - + + + + + + @@ -85,6 +97,7 @@ + @@ -98,6 +111,7 @@ + diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java new file mode 100644 index 000000000..962ad386e --- /dev/null +++ b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java @@ -0,0 +1,46 @@ +package org.argeo.cms.e4.monitoring; + +import org.argeo.eclipse.ui.TreeParent; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + +/** A tree element representing a {@link Bundle} */ +class BundleNode extends TreeParent { + private final Bundle bundle; + + public BundleNode(Bundle bundle) { + this(bundle, false); + } + + @SuppressWarnings("rawtypes") + public BundleNode(Bundle bundle, boolean hasChildren) { + super(bundle.getSymbolicName()); + this.bundle = bundle; + + if (hasChildren) { + // REFERENCES + ServiceReference[] usedServices = bundle.getServicesInUse(); + if (usedServices != null) { + for (ServiceReference sr : usedServices) { + if (sr != null) + addChild(new ServiceReferenceNode(sr, false)); + } + } + + // SERVICES + ServiceReference[] registeredServices = bundle + .getRegisteredServices(); + if (registeredServices != null) { + for (ServiceReference sr : registeredServices) { + if (sr != null) + addChild(new ServiceReferenceNode(sr, true)); + } + } + } + + } + + Bundle getBundle() { + return bundle; + } +} diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java new file mode 100644 index 000000000..1d4a33fd1 --- /dev/null +++ b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java @@ -0,0 +1,129 @@ +//package org.argeo.eclipse.ui.workbench.osgi; +//public class BundlesView {} + +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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 + * + * 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.argeo.cms.e4.monitoring; + +import javax.annotation.PostConstruct; + +import org.argeo.eclipse.ui.ColumnViewerComparator; +import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils; +import org.eclipse.e4.ui.di.Focus; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; + +/** + * Overview of the bundles as a table. Equivalent to Equinox 'ss' console + * command. + */ +public class BundlesView { + private final static BundleContext bc = FrameworkUtil.getBundle(BundlesView.class).getBundleContext(); + private TableViewer viewer; + + @PostConstruct + public void createPartControl(Composite parent) { + viewer = new TableViewer(parent); + viewer.setContentProvider(new BundleContentProvider()); + viewer.getTable().setHeaderVisible(true); + + EclipseUiSpecificUtils.enableToolTipSupport(viewer); + + // ID + TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(30); + column.getColumn().setText("ID"); + column.getColumn().setAlignment(SWT.RIGHT); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -3122136344359358605L; + + public String getText(Object element) { + return Long.toString(((Bundle) element).getBundleId()); + } + }); + new ColumnViewerComparator(column); + + // State + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(18); + column.getColumn().setText("State"); + column.setLabelProvider(new StateLabelProvider()); + new ColumnViewerComparator(column); + + // Symbolic name + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(250); + column.getColumn().setText("Symbolic Name"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -4280840684440451080L; + + public String getText(Object element) { + return ((Bundle) element).getSymbolicName(); + } + }); + new ColumnViewerComparator(column); + + // Version + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(250); + column.getColumn().setText("Version"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = 6871926308708629989L; + + public String getText(Object element) { + Bundle bundle = (org.osgi.framework.Bundle) element; + return bundle.getVersion().toString(); + } + }); + new ColumnViewerComparator(column); + + viewer.setInput(bc); + + } + + @Focus + public void setFocus() { + if (viewer != null) + viewer.getControl().setFocus(); + } + + /** Content provider managing the array of bundles */ + private static class BundleContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = -8533792785725875977L; + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof BundleContext) { + BundleContext bc = (BundleContext) inputElement; + return bc.getBundles(); + } + return null; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } +} diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java new file mode 100644 index 000000000..a85d27bf7 --- /dev/null +++ b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java @@ -0,0 +1,189 @@ +//package org.argeo.eclipse.ui.workbench.osgi; +//public class BundlesView {} + +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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 + * + * 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.argeo.cms.e4.monitoring; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.naming.ldap.LdapName; + +import org.argeo.cms.CmsException; +import org.argeo.cms.auth.CmsSession; +import org.argeo.eclipse.ui.ColumnViewerComparator; +import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils; +import org.argeo.util.LangUtils; +import org.eclipse.e4.ui.di.Focus; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** + * Overview of the active CMS sessions. + */ +public class CmsSessionsView { + private final static BundleContext bc = FrameworkUtil.getBundle(CmsSessionsView.class).getBundleContext(); + + private TableViewer viewer; + + @PostConstruct + public void createPartControl(Composite parent) { + viewer = new TableViewer(parent); + viewer.setContentProvider(new CmsSessionContentProvider()); + viewer.getTable().setHeaderVisible(true); + + EclipseUiSpecificUtils.enableToolTipSupport(viewer); + + int longColWidth = 150; + int smallColWidth = 100; + + // Display name + TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(longColWidth); + column.getColumn().setText("User"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return ((CmsSession) element).getAuthorization().toString(); + } + + public String getToolTipText(Object element) { + return ((CmsSession) element).getUserDn().toString(); + } + }); + new ColumnViewerComparator(column); + + // Creation time + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Since"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return LangUtils.since(((CmsSession) element).getCreationTime()); + } + + public String getToolTipText(Object element) { + return ((CmsSession) element).getCreationTime().toString(); + } + }); + new ColumnViewerComparator(column); + + // Username + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Username"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + LdapName userDn = ((CmsSession) element).getUserDn(); + return userDn.getRdn(userDn.size() - 1).getValue().toString(); + } + + public String getToolTipText(Object element) { + return ((CmsSession) element).getUserDn().toString(); + } + }); + new ColumnViewerComparator(column); + + // UUID + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("UUID"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return ((CmsSession) element).getUuid().toString(); + } + + public String getToolTipText(Object element) { + return getText(element); + } + }); + new ColumnViewerComparator(column); + + // Local ID + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Local ID"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return ((CmsSession) element).getLocalId(); + } + + public String getToolTipText(Object element) { + return getText(element); + } + }); + new ColumnViewerComparator(column); + + viewer.setInput(bc); + + } + + @Focus + public void setFocus() { + if (viewer != null) + viewer.getControl().setFocus(); + } + + /** Content provider managing the array of bundles */ + private static class CmsSessionContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = -8533792785725875977L; + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof BundleContext) { + BundleContext bc = (BundleContext) inputElement; + Collection> srs; + try { + srs = bc.getServiceReferences(CmsSession.class, null); + } catch (InvalidSyntaxException e) { + throw new CmsException("Cannot retrieve CMS sessions", e); + } + List res = new ArrayList<>(); + for (ServiceReference sr : srs) { + res.add(bc.getService(sr)); + } + return res.toArray(); + } + return null; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } +} diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java new file mode 100644 index 000000000..86f84df4e --- /dev/null +++ b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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 + * + * 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.argeo.cms.e4.monitoring; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.argeo.eclipse.ui.TreeParent; +import org.eclipse.e4.ui.di.Focus; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; + +/** The OSGi runtime from a module perspective. */ +public class ModulesView { + private final static BundleContext bc = FrameworkUtil.getBundle(ModulesView.class).getBundleContext(); + private TreeViewer viewer; + + @PostConstruct + public void createPartControl(Composite parent) { + viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); + viewer.setContentProvider(new ModulesContentProvider()); + viewer.setLabelProvider(new ModulesLabelProvider()); + viewer.setInput(bc); + } + + @Focus + public void setFocus() { + viewer.getTree().setFocus(); + } + + private class ModulesContentProvider implements ITreeContentProvider { + private static final long serialVersionUID = 3819934804640641721L; + + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof BundleContext) { + BundleContext bundleContext = (BundleContext) parentElement; + Bundle[] bundles = bundleContext.getBundles(); + + List modules = new ArrayList(); + for (Bundle bundle : bundles) { + if (bundle.getState() == Bundle.ACTIVE) + modules.add(new BundleNode(bundle, true)); + } + return modules.toArray(); + } else if (parentElement instanceof TreeParent) { + return ((TreeParent) parentElement).getChildren(); + } else { + return null; + } + } + + public Object getParent(Object element) { + // TODO Auto-generated method stub + return null; + } + + public boolean hasChildren(Object element) { + if (element instanceof TreeParent) { + return ((TreeParent) element).hasChildren(); + } + return false; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } + + private class ModulesLabelProvider extends StateLabelProvider { + private static final long serialVersionUID = 5290046145534824722L; + + @Override + public String getText(Object element) { + if (element instanceof BundleNode) + return element.toString() + " [" + ((BundleNode) element).getBundle().getBundleId() + "]"; + return element.toString(); + } + } +} diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java new file mode 100644 index 000000000..fe0d3e67c --- /dev/null +++ b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * 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 + * + * 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.argeo.cms.e4.monitoring; + +import org.argeo.cms.ui.theme.CmsImages; +import org.eclipse.swt.graphics.Image; + +/** Shared icons. */ +public class OsgiExplorerImages extends CmsImages { + public final static Image INSTALLED = createIcon("installed.gif"); + public final static Image RESOLVED = createIcon("resolved.gif"); + public final static Image STARTING = createIcon("starting.gif"); + public final static Image ACTIVE = createIcon("active.gif"); + public final static Image SERVICE_PUBLISHED = createIcon("service_published.gif"); + public final static Image SERVICE_REFERENCED = createIcon("service_referenced.gif"); +} diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java new file mode 100644 index 000000000..d9c45fe11 --- /dev/null +++ b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java @@ -0,0 +1,46 @@ +package org.argeo.cms.e4.monitoring; + +import org.argeo.eclipse.ui.TreeParent; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + +/** A tree element representing a {@link ServiceReference} */ +@SuppressWarnings({ "rawtypes" }) +class ServiceReferenceNode extends TreeParent { + private final ServiceReference serviceReference; + private final boolean published; + + public ServiceReferenceNode(ServiceReference serviceReference, + boolean published) { + super(serviceReference.toString()); + this.serviceReference = serviceReference; + this.published = published; + + if (isPublished()) { + Bundle[] usedBundles = serviceReference.getUsingBundles(); + if (usedBundles != null) { + for (Bundle b : usedBundles) { + if (b != null) + addChild(new BundleNode(b)); + } + } + } else { + Bundle provider = serviceReference.getBundle(); + addChild(new BundleNode(provider)); + } + + for (String key : serviceReference.getPropertyKeys()) { + addChild(new TreeParent(key + "=" + + serviceReference.getProperty(key))); + } + + } + + public ServiceReference getServiceReference() { + return serviceReference; + } + + public boolean isPublished() { + return published; + } +} diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java new file mode 100644 index 000000000..5cb5b6563 --- /dev/null +++ b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java @@ -0,0 +1,82 @@ +package org.argeo.cms.e4.monitoring; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.graphics.Image; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; + +/** Label provider showing the sate of bundles */ +class StateLabelProvider extends ColumnLabelProvider { + private static final long serialVersionUID = -7885583135316000733L; + + @Override + public Image getImage(Object element) { + int state; + if (element instanceof Bundle) + state = ((Bundle) element).getState(); + else if (element instanceof BundleNode) + state = ((BundleNode) element).getBundle().getState(); + else if (element instanceof ServiceReferenceNode) + if (((ServiceReferenceNode) element).isPublished()) + return OsgiExplorerImages.SERVICE_PUBLISHED; + else + return OsgiExplorerImages.SERVICE_REFERENCED; + else + return null; + + switch (state) { + case Bundle.UNINSTALLED: + return OsgiExplorerImages.INSTALLED; + case Bundle.INSTALLED: + return OsgiExplorerImages.INSTALLED; + case Bundle.RESOLVED: + return OsgiExplorerImages.RESOLVED; + case Bundle.STARTING: + return OsgiExplorerImages.STARTING; + case Bundle.STOPPING: + return OsgiExplorerImages.STARTING; + case Bundle.ACTIVE: + return OsgiExplorerImages.ACTIVE; + default: + return null; + } + } + + @Override + public String getText(Object element) { + return null; + } + + @Override + public String getToolTipText(Object element) { + Bundle bundle = (Bundle) element; + Integer state = bundle.getState(); + switch (state) { + case Bundle.UNINSTALLED: + return "UNINSTALLED"; + case Bundle.INSTALLED: + return "INSTALLED"; + case Bundle.RESOLVED: + return "RESOLVED"; + case Bundle.STARTING: + String activationPolicy = bundle.getHeaders() + .get(Constants.BUNDLE_ACTIVATIONPOLICY).toString(); + + // .get("Bundle-ActivationPolicy").toString(); + // FIXME constant triggers the compilation failure + if (activationPolicy != null + && activationPolicy.equals(Constants.ACTIVATION_LAZY)) + // && activationPolicy.equals("lazy")) + // FIXME constant triggers the compilation failure + // && activationPolicy.equals(Constants.ACTIVATION_LAZY)) + return "<>"; + return "STARTING"; + case Bundle.STOPPING: + return "STOPPING"; + case Bundle.ACTIVE: + return "ACTIVE"; + default: + return null; + } + } +} diff --git a/org.argeo.cms.ui.theme/icons/logout.png b/org.argeo.cms.ui.theme/icons/logout.png new file mode 100644 index 0000000000000000000000000000000000000000..f2952fa5ba1b492521cef45d22e92f0c203ae719 GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#Xvd=gc;dYI$i@AzdT(W zLo9liPCi?B$UvagUgF(C=F5!l80O!b$+t-84x?TppMva7StN zUw6+nD)Zar%j>pCv^Da^jYM!VW>KG7UyYCYePxED5?4dP((gPNQFtC1b^G z-3@Ggk`7Ogd%sb!SkcH-kan$ZdT!d>_C1WgTm=sEz5NvA4^(q${1L5NBz8>c{5Hcm z*Hm9`w}^24JiTuA&EDr%gL0Ds3NA4JmNefeQ}F$(1f%w@=fU??zny85}Sb4q9e0Nba8Q2+n{ literal 0 HcmV?d00001 -- 2.30.2