Loading services/core/java/com/android/server/connectivity/Tethering.java +81 −67 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ import com.android.internal.util.Protocol; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.server.IoThread; import com.android.server.connectivity.tethering.IControlsTethering; import com.android.server.net.BaseNetworkObserver; import java.io.FileDescriptor; Loading @@ -92,7 +93,7 @@ import java.util.concurrent.atomic.AtomicInteger; * * TODO - look for parent classes and code sharing */ public class Tethering extends BaseNetworkObserver { public class Tethering extends BaseNetworkObserver implements IControlsTethering { private final Context mContext; private final static String TAG = "Tethering"; Loading Loading @@ -264,7 +265,8 @@ public class Tethering extends BaseNetworkObserver { TetherInterfaceSM sm = mIfaces.get(iface); if (up) { if (sm == null) { sm = new TetherInterfaceSM(iface, mLooper, usb, mPublicSync); sm = new TetherInterfaceSM(iface, mLooper, usb, mPublicSync, mNMService, mStatsService, this); mIfaces.put(iface, sm); sm.start(); } Loading Loading @@ -339,7 +341,8 @@ public class Tethering extends BaseNetworkObserver { if (VDBG) Log.d(TAG, "active iface (" + iface + ") reported as added, ignoring"); return; } sm = new TetherInterfaceSM(iface, mLooper, usb, mPublicSync); sm = new TetherInterfaceSM(iface, mLooper, usb, mPublicSync, mNMService, mStatsService, this); mIfaces.put(iface, sm); sm.start(); } Loading Loading @@ -632,7 +635,8 @@ public class Tethering extends BaseNetworkObserver { // TODO - move all private methods used only by the state machine into the state machine // to clarify what needs synchronized protection. private void sendTetherStateChangedBroadcast() { @Override public void sendTetherStateChangedBroadcast() { if (!getConnectivityManager().isTetheringSupported()) return; ArrayList<String> availableList = new ArrayList<String>(); Loading Loading @@ -809,44 +813,6 @@ public class Tethering extends BaseNetworkObserver { Log.e(TAG, "unable start or stop USB tethering"); } // configured when we start tethering and unconfig'd on error or conclusion private boolean configureUsbIface(boolean enabled) { if (VDBG) Log.d(TAG, "configureUsbIface(" + enabled + ")"); // toggle the USB interfaces String[] ifaces = new String[0]; try { ifaces = mNMService.listInterfaces(); } catch (Exception e) { Log.e(TAG, "Error listing Interfaces", e); return false; } for (String iface : ifaces) { if (isUsb(iface)) { InterfaceConfiguration ifcg = null; try { ifcg = mNMService.getInterfaceConfig(iface); if (ifcg != null) { InetAddress addr = NetworkUtils.numericToInetAddress(USB_NEAR_IFACE_ADDR); ifcg.setLinkAddress(new LinkAddress(addr, USB_PREFIX_LENGTH)); if (enabled) { ifcg.setInterfaceUp(); } else { ifcg.setInterfaceDown(); } ifcg.clearFlag("running"); mNMService.setInterfaceConfig(iface, ifcg); } } catch (Exception e) { Log.e(TAG, "Error configuring interface " + iface, e); return false; } } } return true; } // TODO - return copies so people can't tamper public String[] getTetherableUsbRegexs() { return mTetherableUsbRegexs; Loading Loading @@ -1014,7 +980,12 @@ public class Tethering extends BaseNetworkObserver { } } class TetherInterfaceSM extends StateMachine { /** * @hide * * Tracks the eligibility of a given network interface for tethering. */ public static class TetherInterfaceSM extends StateMachine { private static final int BASE_IFACE = Protocol.BASE_TETHERING + 100; // notification from the master SM that it's not in tether mode static final int CMD_TETHER_MODE_DEAD = BASE_IFACE + 1; Loading Loading @@ -1046,6 +1017,10 @@ public class Tethering extends BaseNetworkObserver { private final State mTetheredState; private final State mUnavailableState; private final INetworkManagementService mNMService; private final INetworkStatsService mStatsService; private final IControlsTethering mTetherController; private final boolean mUsb; private final String mIfaceName; Loading @@ -1055,12 +1030,17 @@ public class Tethering extends BaseNetworkObserver { private int mLastError; private String mMyUpstreamIfaceName; // may change over time TetherInterfaceSM(String name, Looper looper, boolean usb, Object mutex) { super(name, looper); mIfaceName = name; TetherInterfaceSM(String ifaceName, Looper looper, boolean usb, Object mutex, INetworkManagementService nMService, INetworkStatsService statsService, IControlsTethering tetherController) { super(ifaceName, looper); mNMService = nMService; mStatsService = statsService; mTetherController = tetherController; mIfaceName = ifaceName; mUsb = usb; setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR); mMutex = mutex; setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR); mInitialState = new InitialState(); addState(mInitialState); Loading Loading @@ -1103,7 +1083,7 @@ public class Tethering extends BaseNetworkObserver { if (mUsb) { // note everything's been unwound by this point so nothing to do on // further error.. Tethering.this.configureUsbIface(false); configureUsbIface(false, mIfaceName); } } } Loading Loading @@ -1139,12 +1119,45 @@ public class Tethering extends BaseNetworkObserver { } } // configured when we start tethering and unconfig'd on error or conclusion private boolean configureUsbIface(boolean enabled, String iface) { if (VDBG) Log.d(TAG, "configureUsbIface(" + enabled + ")"); InterfaceConfiguration ifcg = null; try { ifcg = mNMService.getInterfaceConfig(iface); if (ifcg != null) { InetAddress addr = NetworkUtils.numericToInetAddress(USB_NEAR_IFACE_ADDR); ifcg.setLinkAddress(new LinkAddress(addr, USB_PREFIX_LENGTH)); if (enabled) { ifcg.setInterfaceUp(); } else { ifcg.setInterfaceDown(); } ifcg.clearFlag("running"); mNMService.setInterfaceConfig(iface, ifcg); } } catch (Exception e) { Log.e(TAG, "Error configuring interface " + iface, e); return false; } return true; } private void maybeLogMessage(State state, int what) { if (DBG) { Log.d(TAG, state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what))); } } class InitialState extends State { @Override public void enter() { setAvailable(true); setTethered(false); sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); } @Override Loading @@ -1154,8 +1167,7 @@ public class Tethering extends BaseNetworkObserver { switch (message.what) { case CMD_TETHER_REQUESTED: setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR); mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_REQUESTED, TetherInterfaceSM.this); mTetherController.notifyInterfaceTetheringReadiness(true, TetherInterfaceSM.this); transitionTo(mStartingState); break; case CMD_INTERFACE_DOWN: Loading @@ -1174,16 +1186,15 @@ public class Tethering extends BaseNetworkObserver { public void enter() { setAvailable(false); if (mUsb) { if (!Tethering.this.configureUsbIface(true)) { mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, TetherInterfaceSM.this); if (!configureUsbIface(true, mIfaceName)) { mTetherController.notifyInterfaceTetheringReadiness(false, TetherInterfaceSM.this); setLastError(ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR); transitionTo(mInitialState); return; } } sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); // Skipping StartingState transitionTo(mTetheredState); Loading @@ -1195,10 +1206,9 @@ public class Tethering extends BaseNetworkObserver { switch (message.what) { // maybe a parent class? case CMD_TETHER_UNREQUESTED: mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, TetherInterfaceSM.this); mTetherController.notifyInterfaceTetheringReadiness(false, TetherInterfaceSM.this); if (mUsb) { if (!Tethering.this.configureUsbIface(false)) { if (!configureUsbIface(false, mIfaceName)) { setLastErrorAndTransitionToInitialState( ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR); break; Loading @@ -1216,8 +1226,7 @@ public class Tethering extends BaseNetworkObserver { ConnectivityManager.TETHER_ERROR_MASTER_ERROR); break; case CMD_INTERFACE_DOWN: mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, TetherInterfaceSM.this); mTetherController.notifyInterfaceTetheringReadiness(false, TetherInterfaceSM.this); transitionTo(mUnavailableState); break; default: Loading Loading @@ -1247,7 +1256,7 @@ public class Tethering extends BaseNetworkObserver { if (DBG) Log.d(TAG, "Tethered " + mIfaceName); setAvailable(false); setTethered(true); sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); } private void cleanupUpstream() { Loading Loading @@ -1294,11 +1303,10 @@ public class Tethering extends BaseNetworkObserver { ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR); break; } mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, TetherInterfaceSM.this); mTetherController.notifyInterfaceTetheringReadiness(false, TetherInterfaceSM.this); if (message.what == CMD_TETHER_UNREQUESTED) { if (mUsb) { if (!Tethering.this.configureUsbIface(false)) { if (!configureUsbIface(false, mIfaceName)) { setLastError( ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR); } Loading Loading @@ -1362,9 +1370,9 @@ public class Tethering extends BaseNetworkObserver { break; } if (DBG) Log.d(TAG, "Tether lost upstream connection " + mIfaceName); sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); if (mUsb) { if (!Tethering.this.configureUsbIface(false)) { if (!configureUsbIface(false, mIfaceName)) { setLastError(ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR); } } Loading @@ -1384,7 +1392,7 @@ public class Tethering extends BaseNetworkObserver { setAvailable(false); setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR); setTethered(false); sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); } @Override public boolean processMessage(Message message) { Loading Loading @@ -2104,4 +2112,10 @@ public class Tethering extends BaseNetworkObserver { } pw.decreaseIndent(); } @Override public void notifyInterfaceTetheringReadiness(boolean isReady, TetherInterfaceSM who) { mTetherMasterSM.sendMessage((isReady) ? TetherMasterSM.CMD_TETHER_MODE_REQUESTED : TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, who); } } services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java 0 → 100644 +29 −0 Original line number Diff line number Diff line /* * Copyright (C) 2016 The Android Open Source Project * * 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 com.android.server.connectivity.tethering; import com.android.server.connectivity.Tethering; /** * @hide * * Interface with methods necessary to notify that a given interface is ready for tethering. */ public interface IControlsTethering { void sendTetherStateChangedBroadcast(); void notifyInterfaceTetheringReadiness(boolean isReady, Tethering.TetherInterfaceSM who); } No newline at end of file Loading
services/core/java/com/android/server/connectivity/Tethering.java +81 −67 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ import com.android.internal.util.Protocol; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.server.IoThread; import com.android.server.connectivity.tethering.IControlsTethering; import com.android.server.net.BaseNetworkObserver; import java.io.FileDescriptor; Loading @@ -92,7 +93,7 @@ import java.util.concurrent.atomic.AtomicInteger; * * TODO - look for parent classes and code sharing */ public class Tethering extends BaseNetworkObserver { public class Tethering extends BaseNetworkObserver implements IControlsTethering { private final Context mContext; private final static String TAG = "Tethering"; Loading Loading @@ -264,7 +265,8 @@ public class Tethering extends BaseNetworkObserver { TetherInterfaceSM sm = mIfaces.get(iface); if (up) { if (sm == null) { sm = new TetherInterfaceSM(iface, mLooper, usb, mPublicSync); sm = new TetherInterfaceSM(iface, mLooper, usb, mPublicSync, mNMService, mStatsService, this); mIfaces.put(iface, sm); sm.start(); } Loading Loading @@ -339,7 +341,8 @@ public class Tethering extends BaseNetworkObserver { if (VDBG) Log.d(TAG, "active iface (" + iface + ") reported as added, ignoring"); return; } sm = new TetherInterfaceSM(iface, mLooper, usb, mPublicSync); sm = new TetherInterfaceSM(iface, mLooper, usb, mPublicSync, mNMService, mStatsService, this); mIfaces.put(iface, sm); sm.start(); } Loading Loading @@ -632,7 +635,8 @@ public class Tethering extends BaseNetworkObserver { // TODO - move all private methods used only by the state machine into the state machine // to clarify what needs synchronized protection. private void sendTetherStateChangedBroadcast() { @Override public void sendTetherStateChangedBroadcast() { if (!getConnectivityManager().isTetheringSupported()) return; ArrayList<String> availableList = new ArrayList<String>(); Loading Loading @@ -809,44 +813,6 @@ public class Tethering extends BaseNetworkObserver { Log.e(TAG, "unable start or stop USB tethering"); } // configured when we start tethering and unconfig'd on error or conclusion private boolean configureUsbIface(boolean enabled) { if (VDBG) Log.d(TAG, "configureUsbIface(" + enabled + ")"); // toggle the USB interfaces String[] ifaces = new String[0]; try { ifaces = mNMService.listInterfaces(); } catch (Exception e) { Log.e(TAG, "Error listing Interfaces", e); return false; } for (String iface : ifaces) { if (isUsb(iface)) { InterfaceConfiguration ifcg = null; try { ifcg = mNMService.getInterfaceConfig(iface); if (ifcg != null) { InetAddress addr = NetworkUtils.numericToInetAddress(USB_NEAR_IFACE_ADDR); ifcg.setLinkAddress(new LinkAddress(addr, USB_PREFIX_LENGTH)); if (enabled) { ifcg.setInterfaceUp(); } else { ifcg.setInterfaceDown(); } ifcg.clearFlag("running"); mNMService.setInterfaceConfig(iface, ifcg); } } catch (Exception e) { Log.e(TAG, "Error configuring interface " + iface, e); return false; } } } return true; } // TODO - return copies so people can't tamper public String[] getTetherableUsbRegexs() { return mTetherableUsbRegexs; Loading Loading @@ -1014,7 +980,12 @@ public class Tethering extends BaseNetworkObserver { } } class TetherInterfaceSM extends StateMachine { /** * @hide * * Tracks the eligibility of a given network interface for tethering. */ public static class TetherInterfaceSM extends StateMachine { private static final int BASE_IFACE = Protocol.BASE_TETHERING + 100; // notification from the master SM that it's not in tether mode static final int CMD_TETHER_MODE_DEAD = BASE_IFACE + 1; Loading Loading @@ -1046,6 +1017,10 @@ public class Tethering extends BaseNetworkObserver { private final State mTetheredState; private final State mUnavailableState; private final INetworkManagementService mNMService; private final INetworkStatsService mStatsService; private final IControlsTethering mTetherController; private final boolean mUsb; private final String mIfaceName; Loading @@ -1055,12 +1030,17 @@ public class Tethering extends BaseNetworkObserver { private int mLastError; private String mMyUpstreamIfaceName; // may change over time TetherInterfaceSM(String name, Looper looper, boolean usb, Object mutex) { super(name, looper); mIfaceName = name; TetherInterfaceSM(String ifaceName, Looper looper, boolean usb, Object mutex, INetworkManagementService nMService, INetworkStatsService statsService, IControlsTethering tetherController) { super(ifaceName, looper); mNMService = nMService; mStatsService = statsService; mTetherController = tetherController; mIfaceName = ifaceName; mUsb = usb; setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR); mMutex = mutex; setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR); mInitialState = new InitialState(); addState(mInitialState); Loading Loading @@ -1103,7 +1083,7 @@ public class Tethering extends BaseNetworkObserver { if (mUsb) { // note everything's been unwound by this point so nothing to do on // further error.. Tethering.this.configureUsbIface(false); configureUsbIface(false, mIfaceName); } } } Loading Loading @@ -1139,12 +1119,45 @@ public class Tethering extends BaseNetworkObserver { } } // configured when we start tethering and unconfig'd on error or conclusion private boolean configureUsbIface(boolean enabled, String iface) { if (VDBG) Log.d(TAG, "configureUsbIface(" + enabled + ")"); InterfaceConfiguration ifcg = null; try { ifcg = mNMService.getInterfaceConfig(iface); if (ifcg != null) { InetAddress addr = NetworkUtils.numericToInetAddress(USB_NEAR_IFACE_ADDR); ifcg.setLinkAddress(new LinkAddress(addr, USB_PREFIX_LENGTH)); if (enabled) { ifcg.setInterfaceUp(); } else { ifcg.setInterfaceDown(); } ifcg.clearFlag("running"); mNMService.setInterfaceConfig(iface, ifcg); } } catch (Exception e) { Log.e(TAG, "Error configuring interface " + iface, e); return false; } return true; } private void maybeLogMessage(State state, int what) { if (DBG) { Log.d(TAG, state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what))); } } class InitialState extends State { @Override public void enter() { setAvailable(true); setTethered(false); sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); } @Override Loading @@ -1154,8 +1167,7 @@ public class Tethering extends BaseNetworkObserver { switch (message.what) { case CMD_TETHER_REQUESTED: setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR); mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_REQUESTED, TetherInterfaceSM.this); mTetherController.notifyInterfaceTetheringReadiness(true, TetherInterfaceSM.this); transitionTo(mStartingState); break; case CMD_INTERFACE_DOWN: Loading @@ -1174,16 +1186,15 @@ public class Tethering extends BaseNetworkObserver { public void enter() { setAvailable(false); if (mUsb) { if (!Tethering.this.configureUsbIface(true)) { mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, TetherInterfaceSM.this); if (!configureUsbIface(true, mIfaceName)) { mTetherController.notifyInterfaceTetheringReadiness(false, TetherInterfaceSM.this); setLastError(ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR); transitionTo(mInitialState); return; } } sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); // Skipping StartingState transitionTo(mTetheredState); Loading @@ -1195,10 +1206,9 @@ public class Tethering extends BaseNetworkObserver { switch (message.what) { // maybe a parent class? case CMD_TETHER_UNREQUESTED: mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, TetherInterfaceSM.this); mTetherController.notifyInterfaceTetheringReadiness(false, TetherInterfaceSM.this); if (mUsb) { if (!Tethering.this.configureUsbIface(false)) { if (!configureUsbIface(false, mIfaceName)) { setLastErrorAndTransitionToInitialState( ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR); break; Loading @@ -1216,8 +1226,7 @@ public class Tethering extends BaseNetworkObserver { ConnectivityManager.TETHER_ERROR_MASTER_ERROR); break; case CMD_INTERFACE_DOWN: mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, TetherInterfaceSM.this); mTetherController.notifyInterfaceTetheringReadiness(false, TetherInterfaceSM.this); transitionTo(mUnavailableState); break; default: Loading Loading @@ -1247,7 +1256,7 @@ public class Tethering extends BaseNetworkObserver { if (DBG) Log.d(TAG, "Tethered " + mIfaceName); setAvailable(false); setTethered(true); sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); } private void cleanupUpstream() { Loading Loading @@ -1294,11 +1303,10 @@ public class Tethering extends BaseNetworkObserver { ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR); break; } mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, TetherInterfaceSM.this); mTetherController.notifyInterfaceTetheringReadiness(false, TetherInterfaceSM.this); if (message.what == CMD_TETHER_UNREQUESTED) { if (mUsb) { if (!Tethering.this.configureUsbIface(false)) { if (!configureUsbIface(false, mIfaceName)) { setLastError( ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR); } Loading Loading @@ -1362,9 +1370,9 @@ public class Tethering extends BaseNetworkObserver { break; } if (DBG) Log.d(TAG, "Tether lost upstream connection " + mIfaceName); sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); if (mUsb) { if (!Tethering.this.configureUsbIface(false)) { if (!configureUsbIface(false, mIfaceName)) { setLastError(ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR); } } Loading @@ -1384,7 +1392,7 @@ public class Tethering extends BaseNetworkObserver { setAvailable(false); setLastError(ConnectivityManager.TETHER_ERROR_NO_ERROR); setTethered(false); sendTetherStateChangedBroadcast(); mTetherController.sendTetherStateChangedBroadcast(); } @Override public boolean processMessage(Message message) { Loading Loading @@ -2104,4 +2112,10 @@ public class Tethering extends BaseNetworkObserver { } pw.decreaseIndent(); } @Override public void notifyInterfaceTetheringReadiness(boolean isReady, TetherInterfaceSM who) { mTetherMasterSM.sendMessage((isReady) ? TetherMasterSM.CMD_TETHER_MODE_REQUESTED : TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, who); } }
services/core/java/com/android/server/connectivity/tethering/IControlsTethering.java 0 → 100644 +29 −0 Original line number Diff line number Diff line /* * Copyright (C) 2016 The Android Open Source Project * * 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 com.android.server.connectivity.tethering; import com.android.server.connectivity.Tethering; /** * @hide * * Interface with methods necessary to notify that a given interface is ready for tethering. */ public interface IControlsTethering { void sendTetherStateChangedBroadcast(); void notifyInterfaceTetheringReadiness(boolean isReady, Tethering.TetherInterfaceSM who); } No newline at end of file